paperclipai/paperclip · error
--payload must be a JSON object
Error message
--payload must be a JSON object
What it means
HTTP 404 with body {"error":"Routine not found"} from POST /api/routines/:id/run (manually trigger a routine run). assertCanManageExistingRoutine (routines.ts:108-118) resolves the routine; null (missing or cross-company) maps to this 404 before the assertBoardCanAssignTasks permission gate and svc.runRoutine execute. So a manual run request failed to even locate an actionable routine for this actor.
Solutions
- Preflight GET /api/routines/:id; only call /run when it returns 200.
- Confirm the actor may run routines in that company (board or tasks:assign permission) - otherwise expect 403 after the existence gate.
- For scheduled/manual invocations, resolve the routine by a stable reference (e.g. name lookup from GET /api/routines) instead of a stored ID.
- If the routine was deleted intentionally, remove the schedule/automation that still references it.
Example fix
// before
await api.post(`/api/routines/${routineId}/run`, { input });
// after
const routine = await api.get(`/api/routines/${routineId}`);
if (!routine) {
throw new Error(`routine ${routineId} not found; resolve by name or refresh the runbook`);
}
await api.post(`/api/routines/${routineId}/run`, { input }); Defensive patterns
Strategy: validation
Validate before calling
async function runRoutineSafe(api: ApiClient, routineId: string, input: unknown) {
const res = await api.fetch(`/api/routines/${routineId}`);
if (res.status === 404) {
throw new Error(`routine ${routineId} deleted or cross-company; cannot run`);
}
return api.fetch(`/api/routines/${routineId}/run`, { method: 'POST', body: JSON.stringify(input ?? {}) });
} Type guard
function isApiErrorBody(body: unknown): body is { error: string } {
return typeof body === 'object' && body !== null &&
typeof (body as Record<string, unknown>).error === 'string';
} Try / catch
try {
const run = await api.post(`/api/routines/${id}/run`, input);
} catch (err) {
if (err instanceof ApiError && err.status === 404 && err.body?.error === 'Routine not found') {
throw new RoutineGoneError(id); // caller re-resolves by name/listing
}
throw err;
} Prevention
- Resolve routines by a stable property (name/slug from the listing) for scheduled runs, not stored IDs.
- Require a fresh existence check in 'Run now' handlers right before firing.
- Remove schedules automatically when their target routine 404s.
- Provision running actors with tasks:assign permission to avoid post-404 403 surprises.
When it happens
Trigger: Manually running a deleted routine; an agent key from another company attempting to run a routine; run requests against a stale routine ID after DB reseed; a 'Run now' button in a UI holding a routine that was removed moments earlier.
Common situations: Ops runbooks with hardcoded routine IDs breaking after routine re-creation; schedulers or chat commands firing runs for routines retired between configuration and invocation.
Related errors
- Could not locate local Paperclip skills directory. Expected…
- Environment variable
- Failed to create API key
- Cannot build API path with an empty path segment.
- Challenge secret is required. Pass --token or --token-env.
AI-assisted analysis of paperclipai/paperclip@a7e689b3c3 (2026-08-18).
Data as JSON: /api/errors/b7e2774de317291b.
Report an issue: GitHub.
Appendix: source
Thrown at cli/src/commands/client/agent.ts:873
}
}
}
console.log("");
console.log("# Run this in your shell before launching codex/claude:");
console.log(exportsText);
} catch (err) {
handleCommandError(err);
}
}),
{ includeCompany: false },
);
}
function parseJsonObject(value: string | undefined): Record<string, unknown> | undefined {
if (value === undefined) return undefined;
const parsed = JSON.parse(value) as unknown;
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
throw new Error("--payload must be a JSON object");
}
return parsed as Record<string, unknown>;
}
function parseJson(value: string): unknown {
return JSON.parse(value) as unknown;
}
function parseCsv(value: string | undefined): string[] {
if (!value) return [];
return value.split(",").map((part) => part.trim()).filter(Boolean);
}
View on GitHub (pinned to a7e689b3c3)