paperclipai/paperclip · error · RailwayError
railway_workspace_required
railway_workspace_required
Error message
No authorized Railway workspace was found. Reconnect Railway and select a workspace to enable direct operations.
What it means
Thrown (code railway_workspace_required, HTTP 403) when workspace discovery succeeds but no workspace in the response matches the expected workspace ID schema, or the workspaces array is absent. The connection is authenticated, but no usable authorized workspace exists to scope direct Railway operations.
Solutions
- Reconnect Railway and explicitly select a workspace during consent, as the message instructs
- Ensure the connected Railway account actually belongs to at least one workspace
- Verify workspace membership on the Railway dashboard (accept any pending invites)
- If shapes changed, update the workspace ID validation/adapter
Example fix
// before
const ws = data.workspaces?.find(w => id.safeParse(w?.id).success)?.id;
if (!ws) throw new RailwayError("railway_workspace_required", ...);
// after (user side: create/select a workspace first)
await ensureRailwayWorkspace(account); // create workspace via dashboard if none
const ws = data.workspaces?.find(w => id.safeParse(w?.id).success)?.id; Defensive patterns
Strategy: validation
Validate before calling
async function railwayWorkspaceConfigured(conn) {
const ws = await probeRailwayWorkspaces(conn); // raw discovery
return Array.isArray(ws) && ws.some(w => typeof w?.id === "string" && w.id.length > 0);
} Type guard
function hasAuthorizedWorkspace(data) {
return Array.isArray(data?.workspaces) &&
data.workspaces.some(w => w != null && typeof w.id === "string");
} Try / catch
try {
const ws = await client.workspaceId();
} catch (e) {
if (e?.code === "railway_workspace_required") {
throw new ConfigurationError("Railway connection has no workspace; reconnect and select one.");
} else throw e;
} Prevention
- Always select a workspace during Railway connection consent
- Create at least one workspace on a new Railway account before connecting
- Periodically re-check workspace membership when team roles change
- Validate the stored workspace ID against discovery before operations
When it happens
Trigger: Account has zero workspaces; the only workspaces have missing/malformed IDs failing zod id.safeParse; connection was created for a different account than expected; workspaces field renamed in the MCP response.
Common situations: New Railway account with no workspace yet; revoked workspace membership; Railway MCP response-shape change; user connected with a personal token scoped to an empty org.
Understand the failure class
Background: EmptyResultError / "no results found": when an API or scraper succeeds but returns zero rows — this error's family across 9 libraries.
Related errors
- agent_authorization_required
- paperclip_runner_file_handoff_workspace_unavailable
- railway_api_authorization_required
- railway_api_not_verified
- railway_authorization_required
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/1dab9f27fcdf838c.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/railway.ts:190
let response = await list(headers);
if (response.status === 400) {
await response.body?.cancel();
response = await list(await initializeMcpHttpSession({ send, headers, requestId: "paperclip-railway-workspace-probe" }));
}
if (!response.ok) {
await response.body?.cancel();
throw new RailwayError("railway_workspace_discovery_failed", "Railway's hosted connection is connected, but workspace access could not be checked. Refresh actions to try again.");
}
const body = await boundedResponseText(response, options.signal);
let data: Record<string, any>;
try {
const payload = record(parseMcpHttpResponseBody(body, response.headers.get("content-type")));
const result = record(payload.result);
if (payload.error || result.isError) throw new Error("Workspace discovery failed");
data = record(result.structuredContent ?? JSON.parse(result.content?.find((item: any) => item.type === "text")?.text ?? "{}"));
} catch { throw new RailwayError("railway_workspace_discovery_failed", "Railway could not list authorized workspaces. Refresh actions or reconnect and select a workspace."); }
const workspaceId = Array.isArray(data.workspaces) ? data.workspaces.find((workspace) => id.safeParse(workspace?.id).success)?.id : undefined;
if (!workspaceId) throw new RailwayError("railway_workspace_required", "No authorized Railway workspace was found. Reconnect Railway and select a workspace to enable direct operations.", 403);
return workspaceId;
}
export function createRailwayClient(options: RailwayClientOptions) {
if (!/^Bearer [^\r\n]+$/.test(options.authorization)) throw new RailwayError("railway_authorization_required", "Reconnect Railway to authorize API access.", 401);
const secret = options.authorization.slice(7);
const redact = (value: unknown) => JSON.parse(redactSensitiveText(JSON.stringify(value).split(secret).join("[REDACTED]")));
async function query(document: string, variables: Record<string, unknown>): Promise<Record<string, any>> {
options.signal.throwIfAborted();
let response: Response;
try {
response = await options.request(RAILWAY_API_URL, { method: "POST", redirect: "error", signal: options.signal, headers: { "content-type": "application/json", Authorization: options.authorization }, body: JSON.stringify({ query: document, variables }) });
} catch (error) {
if (options.signal.aborted) throw options.signal.reason;
throw new RailwayError("railway_request_failed", "Railway could not be reached. A deployment request may have succeeded; inspect deployment status before retrying.");
}
if (response.status === 401 || response.status === 403) {View on GitHub (pinned to 3f1d897a7c)