paperclipai/paperclip · error · RailwayError

railway_deployment_ineligible

railway_deployment_ineligible

Error message

Railway does not allow this deployment to be redeployed.

What it means

The `redeploy` Railway operation throws `railway_deployment_ineligible` (HTTP 409) when the target deployment's `canRedeploy` flag is false or missing, as fetched via `validateDeployment` against the Railway GraphQL API. Railway only permits redeploying deployments that are eligible (typically active/recent deployments on services with redeploy enabled); crashed, removed, or very old builds are ineligible. The check runs before the mutation so no partial state is created.

Solutions

  1. Fetch current deployments via `railway_list-deployments` and redeploy the latest eligible deployment instead of the stale one.
  2. Run `railway_deployment-status` for the deploymentId and verify `canRedeploy: true` before invoking redeploy.
  3. If the deployment is permanently ineligible, trigger a new deployment (e.g. push to the connected branch) rather than redeploying.
  4. Confirm the service type supports redeploy (some image/template-based services don't) and update service settings if needed.

Example fix

// before
await railwayCall("railway_redeploy", { projectId, environmentId, serviceId, deploymentId: staleId });
// after
const d = await railwayCall("railway_deployment-status", { projectId, environmentId, serviceId, deploymentId });
if (!d.canRedeploy) {
  const list = await railwayCall("railway_list-deployments", { projectId, environmentId, serviceId, first: 1 });
  deploymentId = list.deployments.edges[0].node.id;
}
await railwayCall("railway_redeploy", { projectId, environmentId, serviceId, deploymentId });
Defensive patterns

Strategy: validation

Validate before calling

const d = await railwayCall("railway_deployment-status", { projectId, environmentId, serviceId, deploymentId });
if (!d || d.canRedeploy !== true) {
  throw new Error(`Deployment ${deploymentId} is not redeployable; select a recent eligible deployment`);
}

Type guard

function canRedeploy(d) {
  return typeof d === "object" && d !== null && d.canRedeploy === true && typeof d.id === "string";
}

Try / catch

try {
  await railwayCall("railway_redeploy", args);
} catch (e) {
  if (e?.code === "railway_deployment_ineligible") {
    // fall back to latest eligible deployment
    const list = await railwayCall("railway_list-deployments", { projectId, environmentId, serviceId, first: 1 });
    await railwayCall("railway_redeploy", { ...args, deploymentId: list.deployments.edges[0].node.id });
  } else throw e;
}

Prevention

When it happens

Trigger: Calling the `railway_redeploy` tool with a `deploymentId` whose `deployment.canRedeploy` field returned false from the RAILWAY_QUERIES.deployment lookup — e.g. redeploying an already-crashed deployment, a deployment for a service whose latest snapshot no longer exists, or an ancient deployment past Railway's redeploy window.

Common situations: Agents or scripts caching an old deploymentId and replaying it days later; attempting to redeploy after a service's settings changed (e.g. image-based or template services that don't support redeploy); picking a superseded deployment from `list-deployments` instead of the latest; redeploying deployments attached to deleted environments or branches.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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

Appendix: source

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

            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;
            const bounded = [];
            for (const line of lines.slice(0, args.limit)) {
              const message = String(line.message ?? "");
              if (message.length > 8192) messageTruncated = true;
              const safe = redact({ timestamp: line.timestamp, severity: line.severity, message: message.slice(0, 8192) });
              bytes += Buffer.byteLength(JSON.stringify(safe));
              if (bytes > 64 * 1024) break;
              bounded.push(safe);
            }
            result = { deploymentId: args.deploymentId, kind: args.kind, lines: bounded, truncated: messageTruncated || bounded.length < lines.length, limitReached: lines.length >= args.limit }; break;
          }
          case "redeploy":
            if (!deployment?.canRedeploy) throw new RailwayError("railway_deployment_ineligible", "Railway does not allow this deployment to be redeployed.", 409);
            result = await query(RAILWAY_QUERIES.redeploy, { deploymentId: args.deploymentId });
            if (!id.safeParse(record(record(result).deploymentRedeploy).id).success) throw new RailwayError("railway_operation_unconfirmed", "Railway did not confirm a resulting deployment. Inspect deployment status before retrying.");
            break;
          case "restart":
            result = await query(RAILWAY_QUERIES.restart, { deploymentId: args.deploymentId });
            if (record(result).deploymentRestart !== true) throw new RailwayError("railway_operation_unconfirmed", "Railway did not confirm the restart. Inspect deployment status before retrying.");
            result = { ...record(result), targetDeploymentId: args.deploymentId };
            break;
          case "rollback":
            if (!deployment?.canRollback) throw new RailwayError("railway_deployment_ineligible", "Railway does not allow rollback to this deployment.", 409);
            result = await query(RAILWAY_QUERIES.rollback, { deploymentId: args.deploymentId });
            if (record(result).deploymentRollback !== true) throw new RailwayError("railway_operation_unconfirmed", "Railway did not confirm the rollback. Inspect deployment status before retrying.");
            result = { ...record(result), targetDeploymentId: args.deploymentId };
            break;
          case "run-command":
            if (!Array.isArray(deployment?.instances) || !deployment.instances.some((entry: { id: string }) => entry.id === args.deploymentInstanceId) || deployment.status !== "SUCCESS") throw new RailwayError("railway_target_mismatch", "The container instance is not part of the selected running deployment.", 403);
            if (!options.runCommand) throw new RailwayError("railway_ssh_setup_required", "Configure Container access on this Railway connection before running commands.", 422);
            result = await options.runCommand({ deploymentInstanceId: args.deploymentInstanceId, command: args.command, timeoutSeconds: args.timeoutSeconds, signal: options.signal }); break;

View on GitHub (pinned to 3f1d897a7c)