Errors & pagination
Reference
Greenlight’s MCP tools and webhooks use the same conventions for errors and pagination. One page; every surface works the same way.
Error envelope
Section titled “Error envelope”Every error response — REST, MCP, webhook delivery failure — uses this flat shape:
{ "code": "app.slug_in_use", "message": "An app with slug 'expense-tracker' already exists in this organization.", "details": { "slug": "expense-tracker" }, "next_steps": "Pick a different slug, or ask the current owner to add you as a co-owner.", "request_id": "req_01HXYZ4M2P"}codeis stable across versions and uses<domain>.<reason>snake_case. Clients should switch oncode, not onmessage.messageis human-readable, one sentence. Greenlight reserves the right to improve wording.details(optional) carries structured, code-specific context — which field failed validation, which check failed in the policy gate, and so on.next_steps(optional) is an actionable hint for the caller.request_idis unique per request. Including it in a support ticket gets to the root cause fastest.
Over MCP the same body comes back as JSON text inside content, with isError: true and no structuredContent — a tool’s advertised output schema describes its success payload, so an error body (which doesn’t match that shape) never goes in structuredContent. Parse content[0].text as JSON to get code / message / next_steps / request_id.
HTTP status mapping
Section titled “HTTP status mapping”| HTTP | When |
|---|---|
400 | Input failed validation, or a connected-database query/response shape was rejected. details identifies ownership and retry safety for SQL calls. |
401 | No valid session, bearer token, or required actor token. details.issuer identifies the configured identity provider when known. Code is auth.token_invalid (missing/malformed) or auth.token_expired (TTL elapsed — re-authenticate). |
403 | Authenticated, but the caller isn’t authorized for this action. |
404 | The target resource doesn’t exist or the caller can’t see it. |
409 | The action conflicts with current state (e.g. a slug in use, or a stale Knowledge base_version). |
429 | The caller’s bucket is exhausted. Retry-After indicates when to retry. details.retry_after_ms carries the same value in milliseconds. |
413 | The explicit request-body cap was exceeded (validation.body_too_large). |
499 | The caller disconnected while connected-database work was queued or active. This is a Greenlight response code for observability, not proof that a write rolled back. |
500 | A platform error. The audit log captures these; report with the request_id. |
502 | An external dependency or its network/auth path failed. Retry only when the code’s contract says the operation is replay-safe; SQL errors state this explicitly. |
503 | A bounded platform resource is temporarily full. The SQL gateway returns proxy.query_capacity with Retry-After. |
Error code catalog
Section titled “Error code catalog”The most common stable codes. Not exhaustive; the full set lives in the platform’s shared error module.
| Code | Meaning |
|---|---|
app.slug_in_use | Another app in the org already owns this slug. |
app.not_found | The app doesn’t exist or the caller can’t see it. |
app.not_owned | Authenticated, but the caller isn’t an owner or co-owner of this app. |
app.invalid_slug | Slug doesn’t match the required pattern. |
app.not_deployed | The app has no live deployment URL yet (e.g. a preview URL was requested before first deploy). |
app.unreachable | curlApp could not complete an HTTP response from the deployed app. Check diagnostics and logs; details.hit_app says whether a response began before the failure. |
permission.scope_denied | Proxy call to an integration the app isn’t granted. |
permission.pending | The app used an integration whose grant is still in IT review. |
proxy.actor_token_required | A user-delegated integration call omitted the actor token. |
proxy.query_failed | A connected-database /query statement was rejected by the database itself (syntax, permission, cast); the message is the database’s own. |
proxy.query_timeout | A connected-database /query statement exceeded the execution-time cap. Narrow it; do not replay a possible write based on this code alone. |
proxy.query_result_unsupported | A query returned a second result grid, too many columns, or an unsafe/unbounded native type. Reshape the result or cast to a bounded scalar type. |
proxy.query_capacity | The SQL execution bulkhead is full. The statement did not start; honor Retry-After. |
proxy.query_canceled | The caller disconnected while SQL work was queued or active. Inspect execution_outcome before reasoning about a write. |
proxy.upstream_unreachable | The connected database or another upstream could not be reached. SQL responses mask the driver message and carry structured ownership/outcome fields. |
internal.audit_unavailable | Greenlight could not persist the required SQL audit event. The database operation may already have committed. |
internal.unexpected | Unexpected platform fault; internal detail is masked. Report the request_id. |
ai.model_denied | The AI request used a model alias not granted to the app. |
auth.session_expired | The session token expired or was revoked. |
auth.token_invalid | No valid bearer token or the token is malformed/revoked. |
auth.token_expired | The bearer token’s TTL elapsed; re-authenticate via the agent plugin. |
rate_limit.exceeded | The caller’s bucket is exhausted. |
env.reserved_name | envSet tried to set a platform-owned name (e.g. DATABASE_URL). |
env.policy_denied | Org policy blocked the env operation. |
validation.body_invalid | Request body failed validation; details carries the issue path. |
Connected-database query errors
Section titled “Connected-database query errors”Every Azure SQL gateway error adds a stable decision record under details:
| Field | Meaning |
|---|---|
fault_origin | The responsible boundary: request (caller SQL/shape), policy (grant or database-role denial), connected_database (the upstream database), platform (Greenlight), or unknown when the boundary cannot prove responsibility. |
retryable | The same logical operation could plausibly succeed later. This is not permission to replay it. |
safe_to_retry | Replaying the identical request cannot duplicate a side effect. Automatically retry only when this and retryable are both true. |
execution_outcome | not_started or unknown. Once execution may have begun, Greenlight does not claim commit or rollback certainty. |
database_code | The SQL Server error number when available. Switch on this field, not database-message prose. |
This is how a client distinguishes platform fault from its own fault: fault_origin: "platform" means Greenlight failed; "request" means the submitted query or result shape needs correction; "policy" means the operation is not allowed; and "connected_database" identifies the external database. "unknown" is intentional when a transport boundary cannot assign blame safely. The same request_id appears in the error body and X-Request-Id response header and correlates to Greenlight logs and audit.
Idempotency
Section titled “Idempotency”Mutating MCP tools accept an idempotency_key. A retried call with the same key and payload returns the original response rather than performing the work twice. Use a fresh UUID v4 per logical operation, and reuse the same one only for retries of that operation.
Pagination
Section titled “Pagination”List endpoints — REST and MCP — use opaque cursor pagination, not offset/limit. Offsets are unsafe under concurrent inserts (they double-count or skip) and get expensive at large offsets. They accept limit (default 20, max 100) and cursor; the response includes next_cursor, or null on the last page.
{ "items": [ /* … */ ], "next_cursor": "eyJrIjoiMjAyNi0wMy0xMlQxNDowODoxMVoiLCJpZCI6IjhhM2IxYzRmIn0="}To paginate forward, pass next_cursor back as cursor. To start from the beginning, omit cursor. There is no offset parameter.
Cursors are opaque: they encode the position needed to resume the query, but the encoding is not part of the API contract. Do not parse a cursor; do not generate one.
Stable sort
Section titled “Stable sort”Each list has a defined sort key — documented per endpoint in the platform API conventions — with a stable tiebreaker so an item appears exactly once during a sustained pagination even as others are added concurrently. Many lists are newest-first by creation time; the apps catalog is newest-deploy first (never-deployed apps last). The cursor encodes that position; you never sort client-side.