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

  1. Preflight GET /api/routines/:id; only call /run when it returns 200.
  2. Confirm the actor may run routines in that company (board or tasks:assign permission) - otherwise expect 403 after the existence gate.
  3. For scheduled/manual invocations, resolve the routine by a stable reference (e.g. name lookup from GET /api/routines) instead of a stored ID.
  4. 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

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


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)