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
- Read the error message appended by getRequestError: it contains the server's message/error field which names the real cause (validation, permissions, duplicate).
- If the error is a ReauthRequiredError (403 error:'reauth'), re-run inside runReauthableAction like useDeleteSkill does, or have the user sign in again.
- Verify pluginId is valid and still exists: GET /v1/config-objects or the plugin query before creating.
- Check the composed rawSourceText via skillSourceFromDraft(draft) — non-empty name/description/body is required by the markdown composer and server validation.
- Check network tab for the actual status; 5xx/HTML pages indicate Den server/proxy problems rather than payload issues.
- 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
- Always render the thrown error's message — getRequestError embeds the server's message/error field with the real cause.
- Compose drafts through skillSourceFromDraft and keep name/body non-empty before submitting.
- Wrap mutations in runReauthableAction when the endpoint can return 403 reauth.
- Keep pluginId fresh from the plugin query, not from stale route state.
- Log response.status server-side/client-side to disambiguate 4xx validation from 5xx outages.
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
- Failed to save skill (${response.status}).
- Failed to delete skill (${response.status}).
- Failed to load organizations (${response.status}).
- Failed to switch organization (${response.status}).
- Failed to load organization (${response.status}).
AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01).
Data as JSON: /api/errors/c1b7ea093f23a2c2.
Report an issue: GitHub.