different-ai/openwork · error

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

Error message

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

What it means

Thrown by useUpdateSkill when POST /v1/config-objects/{skillId}/versions returns a non-ok status. This endpoint creates a new skill version with the composed rawSourceText; the error carries the server's message via getRequestError (and becomes ReauthRequiredError for 403 reauth). It signals the server rejected the version update — the local draft was never persisted.

Source

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

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

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

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

View on GitHub (pinned to 2b7df46e8a)

Solutions

  1. Inspect the appended server message in the thrown error for the concrete cause.
  2. Refresh the skill detail query; if 404, the skill is gone — reload the list instead of retrying the save.
  3. If ReauthRequiredError, wrap the save in runReauthableAction or re-authenticate the user.
  4. Validate the draft (non-empty name/body, reasonable size) before calling mutateAsync.
  5. For concurrent-edit 409s, re-fetch latest version, reapply the edit, and save again.
  6. For 429/5xx retry with backoff.

Example fix

// before: blind save
await updateSkill.mutateAsync({ skillId, draft });
// after: revalidate then save, handling reauth
const latest = await queryClient.fetchQuery(skillQueryKeys.detail(orgId, pluginId, skillId));
if (latest.updatedAt !== loadedAt) throw new Error("Skill changed elsewhere; reload before saving.");
try {
  await runReauthableAction("save-skill", () => updateSkill.mutateAsync({ skillId, draft }));
} catch (err) {
  if (!isReauthRequiredError(err)) throw err;
}
Defensive patterns

Strategy: try-catch

Validate before calling

const skill = skillSourceFromDraft(draft);
if (!draft.name.trim()) throw new Error("Skill name is required.");
if (skill.length > 512 * 1024) throw new Error("Skill source exceeds size limit.");

Type guard

function isReauthRequiredError(e: unknown): e is ReauthRequiredError {
  return e instanceof ReauthRequiredError;
}

Try / catch

try {
  await updateSkill.mutateAsync({ skillId, draft });
} catch (err) {
  if (isReauthRequiredError(err)) { promptSignIn(); return; }
  if (/\b404\b/.test(err.message)) { reloadSkillList(); return; } // deleted elsewhere
  showError(err.message);
}

Prevention

When it happens

Trigger: POST /v1/config-objects/{skillId}/versions with {input:{rawSourceText}, reason:'Updated from Den Web'} returns 400 (invalid skill markdown in the edited draft), 401 (session expired), 403 (caller lacks write access to the skill/org, or reauth challenge), 404 (skillId no longer exists — e.g. deleted in another tab), 409 (version conflict/stale edit), 429, or 5xx. 15s timeout applies.

Common situations: Editing a skill whose underlying config object was deleted or re-created by a teammate; org role downgraded from admin to member mid-session; pasting oversized skill body content; stale open editor after long idle causing token expiry.

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/10f28722e8dd9088. Report an issue: GitHub.