Introduction

OverviewArchitectureAgent Experience

The product

This repository

Structure

Usage

Other

Interfaces

GraphQL API

The planned context-efficient door — a published schema, Relay connections, typed userErrors and a complexity budget

Not built yet. There is no /graphql endpoint and no committed SDL in this repository. Every type and field below is a proposed shape, taken from spec/02-agent-interfaces.md.

One endpoint, POST /graphql, with the same AgentToken authentication, the same scopes and the same state machine as the REST door.

Planned location: apps/api/graphql.

Why both REST and GraphQL

They answer different costs, and neither is the real one.

REST is the lowest-friction door: curl, webhooks, shell scripts, any runtime, no client library.

GraphQL is the context-efficient door. An agent asks for exactly the fields it needs across the task, run, comment and artifact graph in one round trip, instead of making three REST calls whose unused fields it pays for in tokens. For a caller whose scarcest resource is context, that difference is the whole argument.

Both are planned to be generated from the same Zod and Prisma derived schemas, so they cannot drift apart.

The schema is the documentation

Schema-first and published. The SDL is committed to the repository, served at /graphql/schema.graphql, and exposed as an MCP resource at spec://graphql-schema.

Introspection stays on for authenticated tokens. Discovery is the entire point for an agent. It is off for anonymous callers.

Every field, argument and enum value carries a description. For an agent the schema is the documentation, so an undocumented field is an unusable field. Removals go through @deprecated with a reason naming the replacement, never a silent drop.

Relay conventions

  • A Node interface with globally unique ids, which are the ULIDs from the domain model.
  • Cursor Connection and Edge types on every list.
  • first and after only. No offsets, and no unbounded lists.
  • first defaults to 25 and caps at 100, matching the REST pagination limits.

Mutations return payloads, not entities

Every mutation returns a payload type rather than a bare entity:

type TaskUpdatePayload {
  task: Task
  event: Event
  userErrors: [UserError!]!
}

Expected failures — an illegal transition, a stale version, a missing acceptance criterion — arrive as typed userErrors carrying the same code, hint and legal_transitions fields that the REST problem+json response carries.

The top-level errors array is reserved for transport and authentication faults. A caller that has to look in two places to find out whether its write succeeded will eventually look in the wrong one.

Idempotency and concurrency are inputs

Not headers, because GraphQL has one endpoint and one request shape.

mutation {
  taskUpdate(input: {
    id: "tsk_01J..."
    status: IN_REVIEW
    expectedVersion: 7
    idempotencyKey: "b1d0…"
  }) {
    task { id status version }
    userErrors { code hint }
  }
}

The semantics match REST exactly. A replay returns the original payload. A version mismatch returns a conflict_version userError rather than overwriting.

Subscriptions

Over SSE, using graphql-sse:

subscription {
  eventAdded(taskId: "tsk_01J...", since: "evt_01J...") {
    id verb at payload
  }
}

This mirrors the REST event stream, with the same since cursor semantics, so an agent can resume after a restart on whichever door it was already using.

Cost control

An expressive query language given to an agent needs a budget, not a hope.

  • Query depth limit and a complexity budget.
  • A per-token rate limit that charges by complexity rather than by request count.
  • Rejections state what the budget was and what the query cost, so the next query can be made to fit rather than guessed at.
  • Persisted and allowlisted operations for the hot paths a runner uses every loop: claim, heartbeat, finish.

No N+1

Every relation resolves through DataLoader. A resolver that fans out per row fails review — it is one of the explicit gates in the agent experience checklist.

One service layer

The MCP tools are planned as thin wrappers over the same service layer, so a tool result and the equivalent GraphQL selection are byte-identical for the fields they share. An agent that moves between doors does not have to relearn the data.