paperclipai/paperclip · error · RailwayError

railway_invalid_arguments

railway_invalid_arguments

Error message

Invalid Railway operation arguments. Use the exact IDs and limits in the action schema.

What it means

Every operation's parameters are validated with a strict Zod schema before execution; unknown keys, missing required IDs, wrong types, out-of-range paging (`first` outside 1–100), or non-UUID identifiers cause `railway_invalid_arguments` with HTTP 400. The message points callers at the action schema because the schemas are `.strict()` — extra fields are rejected, not ignored.

Solutions

  1. Validate parameters against the operation's inputSchema (exported via RAILWAY_TOOLS as JSON Schema draft-7) before calling
  2. Pass only UUIDs obtained from prior list/status calls and omit optional fields rather than sending nulls
  3. Clamp `first` to 1–100 and read-logs `limit` to 1–500; use `after` cursors returned by previous pages
  4. Remove unknown keys — the schemas are strict, so any extra field fails validation

Example fix

// before
await client.call("paperclip-railway-read-logs", { ...args, limit: 1000, debug: true });
// after
await client.call("paperclip-railway-read-logs", { projectId: p, environmentId: e, serviceId: s, deploymentId: d, kind: "runtime", limit: 500 });
Defensive patterns

Strategy: validation

Validate before calling

const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const ISO_OFFSET = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
const clean = (o: Record<string, unknown>) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== undefined));
// drop null/undefined, clamp paging: first = Math.min(100, Math.max(1, first))

Try / catch

try {
  return await client.call(name, params);
} catch (e) {
  if (isRailwayError(e) && e.code === "railway_invalid_arguments") {
    const schema = operationSchemas[name]; // draft-7 JSON Schema from RAILWAY_TOOLS
    const { valid, errors } = validate(schema, params);
    if (!valid) throw new Error(JSON.stringify(errors)); // fail fast with field detail
  }
  throw e;
}

Prevention

When it happens

Trigger: Omitting required IDs (projectId/environmentId/serviceId/deploymentId); passing `first: 0` or `first: 500`; adding extra keys not in the schema; passing non-UUID strings; passing a limit outside 1–500 or a timeoutSeconds outside 1–60 for run-command; malformed timestamps not matching `z.string().datetime({offset:true})`.

Common situations: Hand-building arguments without consulting inputSchema; serializing optional fields as null instead of omitting them; an agent passing extra context fields it invented; pagination cursor strings longer than 512 chars.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/b161a21a8d6773cf. Report an issue: GitHub.

Appendix: source

Thrown at server/src/services/railway.ts:256

  async function validateDeployment(args: Record<string, any>) {
    const data = await query(RAILWAY_QUERIES.deployment, { deploymentId: args.deploymentId });
    const d = record(data.deployment);
    if (d.id !== args.deploymentId || d.projectId !== args.projectId || d.environmentId !== args.environmentId || d.serviceId !== args.serviceId) throw new RailwayError("railway_target_mismatch", "The deployment does not belong to the selected Railway target.", 403);
    return d;
  }

  return {
    async probe(workspaceId: string) {
      if (!id.safeParse(workspaceId).success) throw new RailwayError("railway_workspace_required", "Choose an authorized Railway workspace before checking API access.", 400);
      await query(RAILWAY_QUERIES.projects, { workspaceId, first: 1 });
    },
    async call(name: string, parameters: unknown): Promise<unknown> {
      if (isRailwayToolBlocked(name)) throw new RailwayError("railway_action_blocked", "This Railway action cannot bind its effects to an approved target. Use redeploy, restart, or rollback for an existing deployment.", 403);
      const operation = name.slice(RAILWAY_TOOL_PREFIX.length) as Operation;
      if (!name.startsWith(RAILWAY_TOOL_PREFIX) || !Object.hasOwn(schema, operation)) throw new RailwayError("railway_unknown_tool", "Unknown Railway operation.", 400);
      const parsed = schema[operation].safeParse(parameters);
      if (!parsed.success) throw new RailwayError("railway_invalid_arguments", "Invalid Railway operation arguments. Use the exact IDs and limits in the action schema.", 400);
      const args = parsed.data as Record<string, any>;
      let result: unknown;
      if (operation === "list-projects") result = await query(RAILWAY_QUERIES.projects, args);
      else if (operation === "list-services" || operation === "list-environments") result = await query(operation === "list-services" ? RAILWAY_QUERIES.services : RAILWAY_QUERIES.environments, args);
      else {
        const instance = await validateTarget(args);
        const deployment = args.deploymentId ? await validateDeployment(args) : null;
        switch (operation) {
          case "service-status": result = instance; break;
          case "deployment-status": result = deployment; break;
          case "list-deployments": result = await query(RAILWAY_QUERIES.deployments, { input: { projectId: args.projectId, environmentId: args.environmentId, serviceId: args.serviceId }, first: args.first, after: args.after }); break;
          case "read-logs": {
            if (args.startDate && args.endDate && Date.parse(args.startDate) > Date.parse(args.endDate)) throw new RailwayError("railway_invalid_arguments", "Log start time must precede end time.", 400);
            const data = await query(args.kind === "build" ? RAILWAY_QUERIES.buildLogs : RAILWAY_QUERIES.runtimeLogs, { deploymentId: args.deploymentId, limit: args.limit, startDate: args.startDate, endDate: args.endDate, filter: args.filter });
            const lines = data[args.kind === "build" ? "buildLogs" : "deploymentLogs"];
            if (!Array.isArray(lines)) throw new RailwayError("railway_invalid_response", "Railway returned invalid log data.");
            let bytes = 0;
            let messageTruncated = false;

View on GitHub (pinned to 3f1d897a7c)