different-ai/openwork · error

Failed to create skill (${response.status}).

Error message

Failed to create skill (${response.status}).

What it means

This error is thrown by useCreateSkill in skill-data.tsx when the POST to /v1/config-objects (the Den server endpoint that creates a skill config object) returns a non-ok HTTP status. getRequestError enriches the fallback message with the server's JSON error payload (payload.message/error) and upgrades 403-with-error:'reauth' responses to a ReauthRequiredError. It means the server rejected the skill creation request, not a client-side parse failure.

Source

Thrown at ee/apps/den-web/app/(den)/dashboard/_components/skill-data.tsx:128

    },
  });
}

export function useCreateSkill(pluginId: string) {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async (draft: SkillDraft): Promise<DenSkill> => {
      const { response, payload } = await requestJson(
        "/v1/config-objects",
        {
          method: "POST",
          body: JSON.stringify(createSkillPayload(pluginId, draft)),
        },
        15000,
      );
      if (!response.ok) {
        throw getRequestError(payload, response, `Failed to create skill (${response.status}).`);
      }
      const skill = parseSkillResponse(payload);
      if (!skill) {
        throw new Error("Skill create response was incomplete.");
      }
      return skill;
    },
    onSuccess: async () => {
      await Promise.all([
        queryClient.invalidateQueries({ queryKey: skillQueryKeys.all }),
        queryClient.invalidateQueries({ queryKey: pluginQueryKeys.detail(pluginId) }),
      ]);
    },
  });
}

export function useUpdateSkill(pluginId: string) {
  const queryClient = useQueryClient();

View on GitHub (pinned to 2b7df46e8a)

Solutions

  1. Read the error message appended by getRequestError: it contains the server's message/error field which names the real cause (validation, permissions, duplicate).
  2. If the error is a ReauthRequiredError (403 error:'reauth'), re-run inside runReauthableAction like useDeleteSkill does, or have the user sign in again.
  3. Verify pluginId is valid and still exists: GET /v1/config-objects or the plugin query before creating.
  4. Check the composed rawSourceText via skillSourceFromDraft(draft) — non-empty name/description/body is required by the markdown composer and server validation.
  5. Check network tab for the actual status; 5xx/HTML pages indicate Den server/proxy problems rather than payload issues.
  6. For 429/5xx add a retry with backoff in the mutation or let the user retry.

Example fix

// before: create without reauth handling
const create = useCreateSkill(pluginId);
await create.mutateAsync(draft); // throws on 403 reauth
// after: guard the mutation like useDeleteSkill
const { runReauthableAction } = useOrgDashboard();
try {
  await runReauthableAction("create-skill", () => create.mutateAsync(draft));
} catch (err) {
  if (!isReauthRequiredError(err)) reportError(err);
}
Defensive patterns

Strategy: try-catch

Validate before calling

const draft = { name: name.trim(), description: description.trim(), body: body.trim() };
if (!draft.name) throw new Error("Skill name is required.");
if (!pluginId) throw new Error("A plugin must be selected before creating a skill.");
const payload = createSkillPayload(pluginId, draft);
if (JSON.stringify(payload).length > 512 * 1024) throw new Error("Skill is too large.");

Type guard

function isReauthRequiredError(e: unknown): e is ReauthRequiredError {
  return e instanceof ReauthRequiredError;
}
function hasServerMessage(e: unknown): e is { message: string } {
  return e instanceof Error && e.message.length > 0;
}

Try / catch

try {
  const skill = await createSkill.mutateAsync(draft);
} catch (err) {
  if (isReauthRequiredError(err)) { promptSignIn(); return; }
  console.error(err.message); // contains HTTP status and/or server error field
  showError(err.message);
}

Prevention

When it happens

Trigger: POST /v1/config-objects with body {type:'skill', sourceMode:'cloud', pluginIds:[pluginId], input:{rawSourceText}} returns 400 (malformed markdown/composed skill source), 401 (expired session token), 403 (no org permission, or reauth required), 404 (pluginId does not exist), 409 (duplicate skill name in plugin), 413 (body too large), 429 (rate limit), or 5xx from an upstream Den failure. The request has a 15s timeout via requestJson.

Common situations: Developer creates a skill from Den Web while their session token expired; the pluginId in the URL/route is stale after the plugin was deleted; org quota or permission limits block the create; the Den server is behind a proxy that returns an HTML 502 page; validation rejects skill body content (e.g. empty name after trimming).

Understand the failure class

Background: 'Something went wrong' / 'Request failed (500)' / 'HTTP error! status: 404' — what failed HTTP requests actually mean and how to find the real cause — this error's family across 28 libraries.

Related errors


AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01). Data as JSON: /api/errors/c1b7ea093f23a2c2. Report an issue: GitHub.