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

  1. 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
  2. Check response body in server logs — the fallback means the server did not return a usable error string; fix or handle the payload shape
  3. Verify apiUrl is correct (no double /api, correct host) so the POST reaches the skills endpoint
  4. 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

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


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)