paperclipai/paperclip · error
Skill creation failed
Error message
Skill creation failed (${response.status}) What it means
Generic Error thrown by callCreateSkillTool when the POST to /api/companies/:companyId/skills returns a non-2xx HTTP status and the response body does not contain a string error field. It wraps the HTTP status code in the fallback message 'Skill creation failed (<status>)'. It surfaces server-side rejection of the skill creation request (validation, auth, policy, or server failure) to the calling agent.
Solutions
- Read the HTTP status in the message: 401/403 → fix the bearer token and company membership; 400/422 → fix the SKILL.md frontmatter so name/description match inputs and satisfy skillFrontmatterSchema; 409 → rename or reuse the existing skill; 5xx → retry after server recovery
- Check response body in server logs — the fallback means the server did not return a usable error string; fix or handle the payload shape
- Verify apiUrl is correct (no double /api, correct host) so the POST reaches the skills endpoint
- Re-validate the input locally with createSkillToolInput (zod) before sending
Example fix
// before
throw new Error(`Skill creation failed (${response.status})`);
// after
throw Object.assign(new Error(`Skill creation failed (${response.status}): ${JSON.stringify(result)}`), { status: response.status });
// or fix the caller: ensure frontmatter name/description equal the tool inputs before POSTing Defensive patterns
Strategy: try-catch
Validate before calling
const parsed = createSkillToolInput.safeParse(args);
if (!parsed.success) return { ok: false, stage: 'local-validation', issues: parsed.error.issues }; Try / catch
try {
return await callCreateSkillTool({ arguments: args, apiUrl, token, companyId });
} catch (e) {
const m = /Skill creation failed \((\d{3})\)/.exec(e.message);
if (m) return { ok: false, status: Number(m[1]), hint: Number(m[1]) < 500 ? 'fix input or auth' : 'retry later' };
throw e;
} Prevention
- Match frontmatter name/description to tool inputs exactly
- Keep markdown under 200000 chars and body nonempty
- Reuse an idempotencyKey deliberately to avoid 409 duplicates
- Confirm the token is for the same companyId in the URL
When it happens
Trigger: POSTing skill creation to the Paperclip API and receiving response.ok === false, e.g. 401/403 (bad or expired bearer token, wrong company), 400/422 (frontmatter/body mismatch, name/slug rules, size limits), 409 (duplicate skill/slug), 5xx (server error), and result.error is not a string (or response.json() returned something unexpected).
Common situations: Agent-generated SKILL.md whose frontmatter name/description do not match the tool inputs; token lacking company skill-creation permission; apiUrl misconfigured so the request hits the wrong route (404); slug collision with an existing skill; server-side policy rejecting the content.
Understand the failure class
Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.
Related errors
- typeof result.error === "string" ? result.error : `Project…
- Discord failed (HTTP ` : ""} ` : ""})
- | null)?.error ?? `Request failed: `}
- OpenCode API request failed
- OpenCode API returned HTTP
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/ef32ff38f2e925a3.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/skill-tools.ts:32
if (!document.hasFrontmatter || !metadata.success || !document.body.trim()
|| metadata.data.name !== input.name || metadata.data.description !== input.description
|| (input.slug !== undefined && input.slug !== input.name)) {
context.addIssue({ code: "custom", path: ["markdown"], message: "Provide a complete SKILL.md with name and description matching the tool inputs, a nonempty body, and slug equal to name when supplied." });
}
});
/** Use the same API and company skill policy as Skill Studio. */
export async function callCreateSkillTool(input: {
arguments: Record<string, unknown>; apiUrl: string; token: string; companyId: string;
}, fetcher: typeof fetch = fetch) {
const body = createSkillToolInput.parse(input.arguments);
const response = await fetcher(`${input.apiUrl.replace(/\/+$/, "").replace(/\/api$/, "")}/api/companies/${encodeURIComponent(input.companyId)}/skills`, {
method: "POST",
headers: { Authorization: `Bearer ${input.token}`, "Content-Type": "application/json" },
body: JSON.stringify(body), signal: AbortSignal.timeout(60_000),
});
const result = await response.json();
if (!response.ok) throw new Error(typeof result.error === "string" ? result.error : `Skill creation failed (${response.status})`);
return { id: result.id, name: result.name, slug: result.slug, description: result.description,
versionId: result.currentVersionId, studioPath: `/skills/studio/${encodeURIComponent(result.id)}` };
}
View on GitHub (pinned to 3f1d897a7c)