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
- Inspect the appended server message in the thrown error for the concrete cause.
- Refresh the skill detail query; if 404, the skill is gone — reload the list instead of retrying the save.
- If ReauthRequiredError, wrap the save in runReauthableAction or re-authenticate the user.
- Validate the draft (non-empty name/body, reasonable size) before calling mutateAsync.
- For concurrent-edit 409s, re-fetch latest version, reapply the edit, and save again.
- 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
- Re-fetch the skill detail before saving to detect concurrent modifications.
- Wrap saves in runReauthableAction to survive 403 reauth challenges.
- Validate the draft (name, size) client-side before POSTing a new version.
- Treat 404 in the error message as 'skill deleted elsewhere' and refresh instead of retrying.
- Keep the editor session short or re-authenticate on idle to avoid expired-token failures.
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
- Failed to create 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/10f28722e8dd9088.
Report an issue: GitHub.