# Management API
Source: https://docs.opencomputer.dev/agents/api
Create sessions, send turns, read the event log and manage projects over HTTP
The management API is the HTTP surface behind the CLI, the dashboard, the
SDK helpers and the React hook's proxy routes. Use it from trusted server
code to run agents from your own application.
## Base URL and authentication
```text theme={null}
https://app.opencomputer.dev/api/managed-agents
```
Every request carries an API key in the `x-api-key` header. A key belongs to
an organization and reaches every project, agent and session in it. Keep it
on the server; the [React integration](/agents/react) shows how a browser
attaches through your own routes without seeing the key.
Your server must authenticate callers and enforce which projects,
environments, agents and sessions they may access. A list filter or session
label is not an authorization boundary.
Request and response bodies are JSON. Paths below are relative to the base
URL; `
` is a project ID and `` a memory resource ID. The
[TypeScript client](/reference/typescript-sdk/agents) on
`@opencomputer/sdk/agents` mirrors these routes one to one.
## Errors
Error bodies are `{ error: { code, message } }`. The `code` is stable and is
what to branch on; the `message` is short and safe to show.
| Status | Meaning |
| - | - |
| `400` | Invalid body, query or header |
| `401` | Missing or invalid API key |
| `402` | `insufficient_credits`: prepaid credits are exhausted |
| `403` | The key may not perform this operation |
| `404` | The route or the target does not exist |
| `409` | The request conflicts with current state, for example a reused `Idempotency-Key` |
| `429` | Too many requests; retry after `Retry-After` |
| `5xx` | Temporary failure; retry with the same idempotency key |
A missing key and an unknown route return `{ "error": "" }` without a
code.
## Sessions
A session is a durable conversation with one deployed agent. It keeps the
deployment it was created on; see [Sessions and turns](/agents/sessions).
| Operation | Method and path | Success |
| - | - | - |
| Create | `POST /sessions` | `201` with the session, `200` when the key had already created it |
| Get | `GET /sessions/` | `200` with the session |
| List | `GET /sessions` | `200` with `{ sessions, nextCursor }`: one page of rows |
| Labels | `PATCH /sessions//labels` | `200` with the session |
| End | `POST /sessions//end` | `200` with the ended session |
| Interrupt | `POST /sessions//interrupt` | `200` with the session |
| Dismiss a question | `POST /sessions//questions//dismiss` | `200`; see [Questions](#questions) |
### Create
```bash theme={null}
curl -X POST 'https://app.opencomputer.dev/api/managed-agents/sessions' \
-H 'x-api-key: ' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: topic/workshop/1' \
-d '{
"agentId": "@development",
"labels": { "topic": "workshop", "requested_by": "u-42" },
"memory": {
"notes": { "scope": "document", "id": "workshop", "access": "read-write" }
}
}'
```
| Field | Meaning |
| - | - |
| `agentId` | `@development` or `@production`. The alias selects the environment and the deployment active in it. A bare agent ID means `production`. This is how applications address an agent; which deployment runs the session is the platform's choice, recorded on the session. |
| `deploymentId` | Advanced: pin one deployment instead of resolving an alias, for reproducibility or testing. Send `environment` with it; it may be omitted only when the deployment is promoted to exactly one environment. |
| `environment` | `development` or `production` for legacy projects; `default` for single-environment projects. Optional with `agentId`; it must agree with the alias. |
| `memory` | Bindings keyed by resource ID, at most eight. Needs a deployed agent and an environment. Shapes on [Document memory](/agents/document-memory#session-bindings). |
| `source` | `api` (default), `playground` or `webhook`. The dashboard groups sessions by it. |
| `labels` | Your own metadata on the session, an object of strings; see [Labels](#labels). Applied at creation and ignored on a replay of the same `Idempotency-Key`. |
| `externalReference` | Your own reference for the session, such as the id of the record it works for: a string of 1 to 256 bytes of UTF-8 without control characters. Stored as given and never interpreted; it is not shown to the agent, grants nothing and does not replace the session id or the `Idempotency-Key`. Returned on the session, its list row and every `session.*` event; see [External references](#external-references). |
The response is `{ session: { id, executionMode, status, createdAt, externalReference? }, deployment }`.
The session starts without a turn; send one with the turns route.
`Idempotency-Key` (at most 256 characters) makes a retry safe. The key
identifies the session within your organization:
* The same key with the same request, meaning the same `agentId` (or the
same pinned `deploymentId`), environment and memory bindings, returns the
existing session with `200`, together with the deployment it pinned when
it was first created. That holds after a redeploy or a rollback: the
session keeps the deployment it started with, and your retry does not
need to know which one that was. A new session under a new key gets the
deployment active at that time.
* Anything else under that key is `409 idempotency_conflict`: another
agent, environment or pinned deployment, different memory bindings, or a
different `externalReference` (including adding or dropping one).
Pinning the deployment the session already holds counts as the same
request.
* Two identical requests under one key that arrive together create one
session; both answers name it, one with `201` and the other with `200`.
* Sessions created before this rule was recorded keep the earlier one: a
replay through an alias succeeds only while the alias still resolves to
the deployment the session pinned.
* `503 memory_admission_unconfirmed` means the memory grants were not
confirmed in time. Retry with the same key; the retry resumes the wait.
* `503 session_publication_unconfirmed` means the session exists but its
row in the list was not confirmed in time; `error.sessionId` names it.
Retry with the same key and the same body until the answer is `201` or
`200`. A success means [`GET /sessions`](#get-and-list) reflects the
session; the platform keeps publishing the row in the background either
way.
Other codes: `400 invalid_environment`, `400 invalid_memory_binding`,
`400 invalid_labels`, `400 invalid_external_reference`, `404
deployment_not_found`, `409 deployment_not_promoted` (a pinned deployment is
not active in the named environment), `402 insufficient_credits`.
### Get and list
`GET /sessions/` returns the session:
| Field | Meaning |
| - | - |
| `id`, `agentId`, `deploymentId` | The session and the deployment it runs on, chosen when it was created |
| `projectId` | The project the agent belongs to |
| `environment` | `development` or `production`, when the request named one |
| `status` | `new`, `connecting`, `idle`, `running`, `stopping`, `waiting_runtime`, `suspending`, `suspended`, `resuming`, `failed` or `ended` |
| `source` | The `source` given at creation |
| `labels`, `labelsUpdatedAt` | Your metadata, and when it last changed; see [Labels](#labels) |
| `externalReference` | The reference given at creation; absent when none was given |
| `memory` | Bindings with `resource`, `scope`, `id`, `access` and `writable`; present when the session was created with bindings |
| `turns` | Every turn: `id`, `input`, `mode`, `status`, `createdAt`, `updatedAt`; `outcome: "question"` on a completed turn that ended by asking; and `deliveries` when an [event subscription](#event-subscriptions) selected its outcome |
| `result` | The latest committed output of the agent's [result tool](/agents/tools#the-session-result), or `null`: `{ turnId, callId, reportedAt, data }`. `turnId` and `callId` name the call that reported `data`, so a reader can tell which turn it came from and whether a later turn changed things without reporting. A failed or cancelled turn never clears it. |
| `question` | The open [question](/agents/sessions#questions) the agent asked with `ask`, or `null`: `{ id, text, options, askedAt }`, `options` an array of `{ label, value }`, empty for free text. Independent of `result`. |
| `revision` | A counter that increases with every change to the session's status, turns, labels or result |
| `createdAt`, `updatedAt` | Timestamps |
`GET /sessions` returns one page of rows, `{ sessions, nextCursor }`, filtered
by the query:
```bash theme={null}
curl 'https://app.opencomputer.dev/api/managed-agents/sessions?project=&environment=development&label.topic=workshop&limit=50' \
-H 'x-api-key: '
```
| Parameter | Meaning |
| - | - |
| `project` or `projectId` | A project id or slug |
| `environment` | `development` or `production` |
| `agent` or `agentId` | An agent id |
| `status` | One session status |
| `deploymentId` | A deployment id |
| `externalReference` | Sessions created with exactly this [external reference](#external-references) |
| `createdAfter`, `createdBefore` | RFC 3339 timestamps: sessions with `createdAt >= createdAfter` and `createdAt < createdBefore` |
| `updatedAfter` | An RFC 3339 timestamp: sessions with `updatedAt >= updatedAfter` |
| `label.=` | Sessions whose label `` equals `` exactly; at most three |
| `limit` | Rows per page, 1 to 100; default 50 |
| `cursor` | The `nextCursor` of the previous page |
Every filter is an exact match; there is no prefix, substring or wildcard
search. Filters combine with AND. Naming both spellings of a parameter
(`project` and `projectId`, `agent` and `agentId`), any other parameter or a
malformed value is `400 invalid_query`.
Filters select among the sessions the API key can already see: an
organization key sees the organization's sessions, a project-scoped key
only its project's, and a filter naming another project is `403
project_scope_violation`. A `development` filter never returns Production
sessions, and a reference created in one project is not found from another.
A row is `id`, `projectId`, `agentId`, `deploymentId`, `environment` (`null`
when the session has none), `source`, `status`, `labels`,
`externalReference` (when one was given), `createdAt`, `updatedAt`,
`revision`, `activity` and `result`. `activity` is
`{ activeTurnId, queued, lastSettledTurn }`: the running turn's id or `null`,
how many turns wait behind it, and the most recently finished turn as
`{ id, status, at }` or `null`. Rows carry no `turns`, no `memory` and no
prompt or message text; read `GET /sessions/` for those.
Rows are ordered by `createdAt`, newest first, then by `id`, so sessions
created in the same millisecond have a fixed order and none is skipped.
Updates do not move rows. `nextCursor` is an opaque string, valid only with
the filters of the page that returned it: sending it with different filters
is `400 invalid_cursor`, and so is a cursor that cannot be read. `limit` may
change between pages. `null` means there are no more matches after that
cursor.
Pagination is not a snapshot: each request applies the filters to the
current rows. New sessions and sessions that newly match a status or label
filter can fall before your cursor. Refresh from the first page to see
them. Sorting fetched rows by `updatedAt` orders only those rows, not all
matching sessions. A successful create response confirms the session is
listed.
### External references
`externalReference` is one opaque string your application chooses when it
creates a session, for example the id of the order, ticket or job the
session works for. It is 1 to 256 bytes of UTF-8 with no control characters;
anything else is `400 invalid_external_reference`. Omitting it, or sending
`null`, creates a session without one; it cannot be changed afterwards, and
it is not required to be unique.
The platform stores the reference as given. It is not sent to the agent or
the model, does not grant access, and does not replace the session id or the
`Idempotency-Key`. Keep your own mapping from reference to session id as the
authoritative record; the reference is there for recovery and audit when
that record is incomplete:
* If a create response was lost, retry with the same `Idempotency-Key`
first; it returns the same session with `200`. The reference makes
a second recovery path available: `GET /sessions?externalReference=`
finds the session however many unrelated sessions were created since.
* A retry of the key with a different reference is `409
idempotency_conflict`, so two records can never share one session by
accident.
* The reference is returned on the session, on its list row and in the
`data` of every `session.*` [event](/agents/events), so a consumer of the
event log can route each event to the record it belongs to without
another lookup.
A reference is metadata, not a credential: do not put secrets, tokens or
signed URLs in it. It is visible to every API key that can read the
session.
### Labels
Labels are your own metadata on a session: at most 16 keys, each matching
`^[a-z][a-z0-9_.-]{0,63}$`, each value at most 256 characters, 4 KB in all.
They filter the list and appear on the session and its row. They are not
permissions, are not searched beyond equality, and are never shown to the
agent; give the agent its context in the turn input.
`PATCH /sessions//labels` changes them and returns the session:
```bash theme={null}
curl -X PATCH 'https://app.opencomputer.dev/api/managed-agents/sessions//labels' \
-H 'x-api-key: ' \
-H 'Content-Type: application/json' \
-d '{ "set": { "outcome": "merged" }, "unset": ["requested_by"] }'
```
`unset` removes keys, then `set` writes keys; each key takes the last write.
The session's `labelsUpdatedAt` records the change. A patch that would exceed
the bounds, or a key or value that does not fit them, is `400 invalid_labels`
and changes nothing. Labels can be changed on an ended session.
A `200` means the list reflects the change: the session's row carries the
new labels and the filters see them. `503 session_publication_unconfirmed`
(with `error.sessionId`) means the labels are recorded on the session but
the row was not confirmed in time; retry the same patch until it answers
`200`. The platform keeps publishing the row in the background either way,
and repeating the patch is safe because each key takes the last write.
### End and interrupt
`POST /sessions//end` ends the session. Queued and running turns are
marked `cancelled` with reason `session_ended`, and no further turns are
accepted. A `200` confirms memory write access is revoked.
`503 session_end_unconfirmed` means the session ended but memory revocation
was not acknowledged; retry the same end request, or
[freeze the document](/agents/document-memory#disable-agent-writes).
Repeating end confirms any pending memory revocation without ending the
session again.
The `session.ended` event is recorded before memory revocation is confirmed.
Neither that event nor the end response waits for commands to stop; remote
cleanup continues in the background. Interrupt provides command-settlement
confirmation for the current turn, subject to the compatibility note below;
it leaves queued turns available to run.
`POST /sessions//interrupt` stops the running turn without spending a
model turn. The response acknowledges the stop request; it does not wait
for completion. While stopping, the session is `stopping` and the turn
remains `running`. Once the model loop and its commands have stopped, the
turn becomes `cancelled` with reason `interrupted`, the session becomes
`idle`, and the next queued turn can start.
If a command cannot be confirmed stopped, OpenComputer terminates its
computer before settling the turn. The next command uses a fresh computer
with the session's workspace. The cancellation event records the
[settlement details](/agents/events#turns). An idle session is returned
unchanged. Stopping does not undo files already written or external effects.
> [!NOTE]
>
> Sessions created with `executionMode: "microvm"` cancel immediately on
> interrupt, without waiting for their commands to stop. They do not enter
> `stopping`, and their cancellation events have no settlement fields.
> Check the creation response when this guarantee matters to your app.
## Turns
```bash theme={null}
curl -X POST 'https://app.opencomputer.dev/api/managed-agents/sessions//turns' \
-H 'x-api-key: ' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: message-42' \
-d '{
"input": "Plan the workshop.",
"mode": "queue",
"payload": { "workshopId": "ws_812", "audience": "backend" }
}'
```
`Idempotency-Key` is the one rule on both routes: it identifies this request,
a repeat returns the existing turn, and a different request under the same
key is refused (below). Without one every request starts a turn. The body
field `idempotencyKey` carries the same key for a send that a browser makes
through your own server, where the header is the server's to set; when both
are present they must be equal, else `400 invalid_turn`.
| Field | Meaning |
| - | - |
| `input` | The user text for this turn. Required, not empty. |
| `idempotencyKey` | Optional. The key in body form, for a proxied browser send; equal to the header when both are present. |
| `mode` | `queue` (default): run after earlier turns. `steer`: deliver into the running turn when the runtime supports it, otherwise queue. `interrupt`: interrupt running turns and run next, with the [same settlement behavior](#end-and-interrupt) as the interrupt route. |
| `payload` | Optional. A JSON value for the agent, at most 32 KB encoded. It reaches the agent as [`useInput().payload`](/agents/inputs) with `source: "user"`, next to the text, and is recorded on the turn's `message.received` event. The text stays the message; the payload is structured context the agent reads. |
| `answers` | Optional. The id of the session's open [question](/agents/sessions#questions). The input answers it, and the agent reads it as [`useInput().answer`](/agents/inputs#answers-and-steering). |
The response is `202 { turnId, status, duplicate }` for a new turn and
`200` with `duplicate: true` when the key had already created one. `status`
is the turn's status: `queued` or `running` for a new turn, and for a
repeated key whatever the existing turn has reached, `completed`, `failed`
or `cancelled` once it has settled. Follow the turn in the event log; the
response does not wait for it.
The key identifies the request, not only the call. A repeat with the same
`input`, `payload` and `mode` is the existing turn: key order inside the
payload does not matter, an omitted `mode` means `queue`, and whitespace in
the text counts. The same key with anything else is `409
idempotency_conflict` and admits nothing. A retry after a lost reply is
therefore safe with the same body, and a different request never lands on
another request's key.
Codes: `400 invalid_turn`, `400 invalid_payload` (over 32 KB),
`402 insufficient_credits`, `409 idempotency_conflict`,
`409 question_stale`,
`409 memory_admission_pending` (retry the session creation with its key
first), `409 memory_admission_rejected` (the session is ended; create a new
one).
### Questions
While the session has an open [question](/agents/sessions#questions), a turn
sent without `answers` is held rather than run: the response is
`202 { status: "held", questionId, duplicate }` with no `turnId`, and `200`
for a repeated key. The held input is delivered with the answer, in order, as
the answering turn's `steering`, up to a bounded number and 64 KiB; the rest
run as ordinary turns after it. A repeat of its key after delivery returns the
receipt of the turn that carried it; after a [Linear](/agents/linear#stop)
stop, it returns `status: "discarded"`. A turn already queued when the
question was asked keeps its `turnId` and settles as `cancelled` with
reason `held` and the `questionId`.
A turn with `answers` resolves the question and runs as the answer, in one
step, idempotent on its key; the same key with a different `answers` is
`409 idempotency_conflict`. Naming a question that is not the open one, or
answering when none is open, is `409 question_stale`; read the session and
answer its current question.
`POST /sessions//questions//dismiss` closes the open question
with reason `dismissed`; its held inputs run as ordinary turns, in order.
Ending the session closes it with `ended`. Interrupt does not close it.
### Start work from an application
An application that turns a form submission into agent work makes two calls,
each with its own key, and keeps what it sent until the second one is
acknowledged:
1. `POST /sessions` with `Idempotency-Key: ` and
`agentId: @`. The platform chooses the deployment
active in that environment and records it on the session; a retry under
the same key returns that session, even if the alias has moved since.
The application never discovers, verifies or stores a deployment id.
2. `POST /sessions//turns` with `Idempotency-Key: /start`,
the text and the `payload`.
Keep the submission (its id, the text and the payload) until the turns call
answers, with `duplicate: false` or `true`.
A lost reply at either step is retried with the same body and the same key:
the first step returns the same session, the second the same turn. A
session that exists without a turn is work that was never admitted; only the
holder of the submission can retry it. A `409 idempotency_conflict` from
either call means a different request already used that id; show it, keep
the draft, and do not append the submission to a session it did not create.
Each later message is its own submission with its own key.
## Events
```bash theme={null}
curl 'https://app.opencomputer.dev/api/managed-agents/sessions//events?after=0' \
-H 'x-api-key: '
```
`GET /sessions//events?after=` returns `{ events }`: up to 500
events with `seq` greater than `after`, in ascending order. Each event is
`{ id, seq, timestamp, sessionId, turnId, type, data }`; `turnId` is absent
on session-level events.
To read a whole log, start at `after=0` and repeat with the last `seq` you
received until a page is empty. A full page means more may follow; read on
without waiting. To follow a live session, keep polling from the last
`seq`; the CLI's `sessions tail` and `useAgent` do exactly this. The log is
durable, so a consumer that stops can resume from its cursor and miss nothing.
Event types and their `data` are listed on [Session events](/agents/events).
## Memory
Document memory belongs to a project and an environment. Every route takes
`?environment=development` or `?environment=production`. Bodies, conditional
headers, the document object and the error codes are on
[Document memory](/agents/document-memory#management-api).
| Operation | Method and path | Success |
| - | - | - |
| Resource inventory | `GET /projects/
/memory` | `200` with `{ resources }` |
| List documents | `GET /projects/
/memory//documents/` with `If-Match` | `204` |
```bash theme={null}
curl -X PUT 'https://app.opencomputer.dev/api/managed-agents/projects//memory/notes/documents/workshop?environment=development' \
-H 'x-api-key: ' \
-H 'Content-Type: application/json' \
-H 'If-None-Match: *' \
-d '{ "title": "Workshop notes" }'
```
`oc.sessions.startOnDocument` on the
[TypeScript client](/reference/typescript-sdk/agents) does this
create and the session create in one call.
## Webhooks
Webhook configuration lives under the project; invocation uses the separate
`/api/agent-webhooks//` URL described on
[Agent webhooks](/agents/webhooks).
| Operation | Method and path | Body or query | Success |
| - | - | - | - |
| List | `GET /projects/
/webhooks` | Optional `?environment=` and `?agentId=` | `200` with `{ webhooks }`, URLs without tokens |
| Create | `POST /projects/
/webhooks` | `{ name, agentId, environment, identity? }` | `201` with `{ webhook }`; `token` and the full `invocationUrl` appear once |
| Update | `PATCH /projects/
/webhooks/` | Any of `name`, `enabled`, `identity` (`null` removes it) | `200` with `{ webhook }` |
| Rotate token | `POST /projects/
/webhooks//rotate-token` | None | `200` with `{ webhook }` carrying the new token |
| Delete | `DELETE /projects/
/webhooks//requests` | None | `200` with `{ requests }` |
`identity` is `header:` or `body:`. Creating a webhook
for an agent that is not deployed in the environment is
`409 webhook_target_unavailable`.
```bash theme={null}
curl 'https://app.opencomputer.dev/api/managed-agents/projects//webhooks//requests' \
-H 'x-api-key: '
```
## Event subscriptions
An event subscription delivers the recorded outcome of turns run by agents
in a project to a session in the same project, as a new turn of that
session. It is how one agent learns that another finished: a coordinator
subscribes to its workers' outcomes and reasons about them when they arrive.
Destinations are sessions only; there is no public HTTPS destination, and
this is not the [outbound webhooks](/agents/webhooks) feature.
| Operation | Method and path | Success |
| - | - | - |
| Create | `POST /projects/
/event-subscriptions` | `201` with `{ subscription }` |
| List | `GET /projects/
/event-subscriptions` | `200` with `{ subscriptions }` |
| Get | `GET /projects/
/event-subscriptions/` | `204`; pending deliveries stop |
```bash theme={null}
curl -X POST 'https://app.opencomputer.dev/api/managed-agents/projects//event-subscriptions' \
-H 'x-api-key: ' \
-H 'Content-Type: application/json' \
-d '{
"agentId": "worker",
"events": ["turn.completed", "turn.failed"],
"destination": { "type": "session", "sessionId": "" },
"environment": "development"
}'
```
| Field | Meaning |
| - | - |
| `agentId` | Optional. Outcomes of this agent only; omitted, every agent in the project. |
| `events` | One or more of `turn.completed`, `turn.failed`, `turn.cancelled`. Turn outcomes, not session ends. |
| `destination` | `{ type: "session", sessionId }`: a session of an agent in this project that can still take turns. |
| `environment` | `development` or `production`. A subscription is scoped to one environment: it selects outcomes recorded there and its destination session runs there. |
A subscription is immutable; `{ subscription }` carries `id`, `projectId`,
the fields above and `createdAt`. Codes: `400 invalid_event_subscription`,
`403 project_scope_violation` (the agent is outside the project),
`404 destination_session_not_found`, `409 destination_session_ended`,
`404 event_subscription_not_found`.
### Delivery and receipts
When a selected turn settles, the source session records one delivery per
matching subscription and starts a turn on the destination session with
`source: "event"` input; the receiving agent reads it with `useInput()`
([Event input](/agents/inputs#event-input)). The destination turn's
idempotency key is `:`, so a retried delivery
never starts a second turn. Deliveries are retried with backoff and stop
when the subscription is deleted or the destination has ended.
`GET /sessions/` on the source session lists each turn's `deliveries`:
| Field | Meaning |
| - | - |
| `id` | `:` |
| `subscriptionId`, `eventId`, `eventType` | What was delivered, to which subscription |
| `destination` | The subscription's destination |
| `status` | `pending`, `delivered` or `failed` |
| `attempt` | Delivery attempts so far |
| `receipt` | `{ sessionId, turnId }`: the turn the destination admitted, once delivered |
| `nextAttemptAt` | When a pending delivery is retried |
| `error` | `subscription_unavailable`, `target_missing`, `target_ended`, or `delivery_failed` |
The delivered event names identifiers and the outcome; a completed turn's
final message is included, bounded to 16 KB. The receiving agent should
treat that text as data about another agent's work, not as instructions.
## Linear connections
A [Linear connection](/agents/linear) binds a Linear app in your workspace to
one agent in one environment. The dashboard drives these routes. A
project-scoped key may list its own project's connections; every other route
needs an organization API key and is `403 forbidden` otherwise.
| Operation | Method and path | Body or query | Success |
| - | - | - | - |
| Create | `POST /projects/
/linear/connections` | `{ name, environment, agentId }` | `201` with `{ connectionId, webhookUrl, createAppUrl, connection }`; `200` when a pending connection for that binding already exists |
| List | `GET /projects/
/linear/connections` | Optional `?environment=` | `200` with `{ connections }` |
| Set credentials | `PUT /linear/connections//credentials` | `{ clientId, clientSecret, signingSecret }`, as copied from the Linear app | `200` with `{ connection }` |
| Authorize | `POST /linear/connections//authorize` | None | `200` with `{ authorizeUrl, expiresAt }` |
| Disconnect | `DELETE /linear/connections/` | None | `200` with `{ connection, revoked }` |
`createAppUrl` opens Linear's create-app page prefilled with the name, the
callback URL and `webhookUrl`. Creating again for a pending binding resets it
with the new name and a new `webhookUrl`, so an earlier `createAppUrl` stops
working. `webhookUrl` contains the connection's secret;
treat it as a credential. `authorizeUrl` is Linear's consent page for the app;
a workspace admin completes it, and Linear returns to OpenComputer's callback,
which stores the tokens and redirects to the project's Connections tab.
`revoked` on a disconnect says whether Linear confirmed the token revocation.
A connection is `{ id, projectId, environment, agentId, name, status,
clientId, appUserId, organizationId, webhookUrl, createAppUrl, verifiedAt,
verificationError, lastEventAt, teams, health, revision, createdAt,
updatedAt }`. It never carries a secret or a token. `status` is `pending`,
`connected`, `disconnected` or `revoked`. `health` is `{ state, message,
lastEventAt? }`, with `state` one of `awaiting_credentials`,
`awaiting_authorization`, `waiting_for_first_delegation`, `receiving` or
`revoked`. `teams` is `{ allPublic, teamIds }`, the teams the app can see.
| Code | Meaning |
| - | - |
| `400 invalid_environment`, `400 invalid_agent` | `environment` must be `development` or `production`; `agentId` must name an agent of the project |
| `400 invalid_linear_app_name` | The name must be 1 to 64 characters |
| `400 linear_app_name_reserved` | Linear does not allow app names that contain "Linear" |
| `400 invalid_linear_credentials` | `clientId`, `clientSecret` and `signingSecret` are all required |
| `403 forbidden` | A project-scoped key on a route other than List |
| `404 project_not_found` | No such project for this key |
| `404 linear_connection_not_found` | No such connection, or it was disconnected |
| `409 project_archived` | Restore the project before creating a connection |
| `409 linear_already_connected` | The agent already has a connection in this environment; `error.connectionId` names it |
| `409 linear_app_already_connected` | That Linear app is connected to another agent or environment; create a separate app |
| `409 linear_connection_connected` | The connection is authorized with a different Linear app; disconnect it first |
| `409 linear_credentials_required` | Set the credentials before authorizing |
| `409 linear_connection_changed` | Another request changed the connection; list the connections and continue; `error.connectionId` names it |
| `503 linear_connector_unavailable` | Linear connections are not available right now |
## Projects, agents and deployments
| Operation | Method and path | Success |
| - | - | - |
| List projects | `GET /projects` | `200` with `{ projects }` |
| Create project | `POST /projects` with `{ name, slug?, environmentMode? }` | `201` with the project. `environmentMode` is `single` (one `default` environment) or `legacy` (Development and Production) |
| Get project | `GET /projects/
` | `200` with `{ project, deployments, sessions, connections, schedules }` |
| List agents | `GET /agents` | `200` with `{ agents }`: `id`, `name`, `activeAlias`, `activeDeploymentId`, `deploymentCount`; `activeAlias` and `activeDeploymentId` are `null` while the agent has no active deployment |
| List deployments | `GET /deployments?agentId=` | `200` with `{ deployments }` |
| Get deployment | `GET /deployments/` | `200` with `id`, `agentId`, `alias`, `memory` declarations, `createdAt` |
| List GitHub repositories | `GET /projects/
/github/repositories?environment=` | `200` with `{ repositories, nextCursor }`; see [GitHub App connections](/agents/github#list-the-repositories-an-environment-can-reach) |
A project is `{ id, slug, name, environmentMode, environments, agents, createdAt, updatedAt }`;
`agents` carries each agent's `id` and `name`. Single-environment projects have one
environment named `default`; requests that name `development` resolve to it,
and requests that name `production` fail with `409 single_environment_project`. See
[Projects and agents](/agents/projects) for how projects are created and
linked from the CLI.
```bash theme={null}
curl 'https://app.opencomputer.dev/api/managed-agents/projects' \
-H 'x-api-key: '
```
Secrets, runtime variables, schedules and logs are managed with the CLI and
the dashboard; see their pages under Concepts.
# Bring your own model account (BYOK)
Source: https://docs.opencomputer.dev/agents/byok
Use a Codex account for model inference in Serverless Agents
BYOK lets a Serverless Agent use an organization-connected Codex account for
eligible OpenAI models. OpenComputer keeps the authorization in its control
plane; it is never added to your deployment, agent source, or runtime
environment.
BYOK is available on the **Pro and Max plans**. Runtime compute is still charged
to your OpenComputer account. Model calls routed through your Codex account have
no OpenComputer model-inference charge and remain subject to the provider's
allowances and terms.
## Connect a Codex account
Only an organization admin can connect or replace the organization account.
From a linked project repository, run:
```bash theme={null}
npx --yes --package=@opencomputer/cli@latest -- opencomputer model-access connect codex
```
To choose a project explicitly, including when you are outside its repository,
add its slug:
```bash theme={null}
npx --yes --package=@opencomputer/cli@latest -- opencomputer model-access connect codex --project my-project
```
The CLI opens the authorized OAuth flow in your browser, stores the account for
the organization, and enables it for both development and production in the
selected project. The CLI exits after the callback completes.
An organization has one Codex account. Connecting again replaces that account
for every project that uses it.
## Enable an existing account for another project
Open the project's **BYOK** page and select **Enable for this project**. You can
also run the connect command with `--project`; if the organization account is
already connected, OpenComputer enables the project without requiring another
OAuth flow.
Project enablement always covers both development and production. Select
**Disable for this project** to return only that project to Managed inference,
or disconnect the organization account to stop using it in every project.
## Select a Codex model
Use the native `openai` provider in agent code:
```tsx theme={null}
import { useModel } from "@opencomputer/agent";
export default function Agent() {
useModel({ provider: "openai", model: "gpt-5.6-sol" });
return "You are a helpful assistant.";
}
```
The debug inspector labels a successful subscription-funded route **Codex
account · BYOK**. If no connected and enabled account can serve the request,
OpenComputer uses Managed inference when available. Managed fallback model
calls are charged normally and are shown as Managed in session and usage
details.
To request an OpenAI model through Managed OpenRouter explicitly, use its full
OpenRouter identifier instead:
```tsx theme={null}
useModel({ provider: "openrouter", model: "openai/gpt-5" });
```
## Troubleshooting
* **The BYOK page asks you to upgrade:** the organization must be on Pro or Max.
* **The organization account is connected but the project is not enabled:**
select **Enable for this project** or rerun the CLI command with `--project`.
* **The account is revoked or needs authentication:** rerun the connect command
to replace it.
* **A session used Managed inference:** confirm the project is enabled and that
agent code uses `provider: "openai"` with a Codex-supported model.
See [Models](/agents/models) for the general `useModel()` contract.
# Agent capabilities
Source: https://docs.opencomputer.dev/agents/capabilities
Choose tools, MCP servers, skills, memory and outbound connections
Capabilities give an agent something beyond its instructions and model. Choose
the narrowest capability that fits the job.
| Capability | Use it for | Where the work happens | How it is attached |
| - | - | - | - |
| Code-defined tool | One typed operation you implement | Your TypeScript `run()` function | `useTool(tool)` |
| Tool that waits for approval | A write a person should agree to first | Your `preview()` shows it; your `apply()` runs after a decision | `useTool(tool)` — the same tool, defined with `preview`/`apply` instead of `run` |
| Connected service | Google services, GitHub, or Linear, on someone's connected account | OpenComputer attaches and refreshes the credential | `useService("gmail")`, then `callService()` in a tool |
| MCP server | A service that already publishes a collection of tools | The remote MCP server | `useMcpServer(server)` |
| Skill | A reusable procedure, checklist, or supporting files | The agent follows the loaded instructions | Place it under `skills/` |
| HTTP connection | A constrained authenticated outbound request | OpenComputer sends the declared request | Call `defineConnection().fetch()` inside a tool |
| GitHub App connection | Direct Git and GitHub API access to an installation | The agent sandbox uses a short-lived installation token | `useConnection(github)` |
| Memory | Notes shared across sessions | OpenComputer stores documents; the agent reads and saves through memory tools | `useMemory(memory)` |
`useModel()` selects the model for a render, but it does not add an executable
capability.
## Tool or MCP server?
Use a [code-defined tool](/agents/tools) when you own the operation, want a
small schema, or need application-specific validation. Use an
[MCP server](/agents/mcp) when another service already hosts the tools.
Both ultimately give the model callable tools. A code-defined tool exposes the
single operation you register; an MCP server exposes the tools it advertises.
## Tool or skill?
A tool performs an operation and returns structured data. A
[skill](/agents/skills) teaches the agent how to approach a category of work.
Many agents use both: the skill describes the workflow while tools perform the
individual actions.
## Session data or memory?
[Session data](/agents/session-data) describes the current session and is set
by your application. [Document memory](/agents/document-memory) keeps notes
across sessions that agents can save and owners can edit. A tool that
writes to your own database is a third option when the data belongs to your
application rather than to the agent's continuing work.
## HTTP connections are not managed accounts
`defineConnection()` declares a secret-backed HTTP destination. It is not an
OAuth account selector and does not itself give the model a tool. Call it from
a code-defined tool or attach it to an MCP definition that accepts the
declared authorization headers. The proposed [HTTP memory provider
contract](/agents/memory-providers) uses the same connection mechanism.
A [managed GitHub App connection](/agents/github) is the exception: the user
installs OpenComputer's App, chooses repositories in GitHub, and the eligible
agent receives a short-lived installation token for direct Git and API calls.
See [Secrets and outbound requests](/agents/secrets) for the complete security
model.
# Database
Source: https://docs.opencomputer.dev/agents/database
A durable SQL database shared by every session in a project environment
Every project environment has one durable SQL database. All agents and
sessions in that environment share it, so an agent can save structured state
in one session and read it in another. Development and Production databases
are isolated.
OpenComputer provisions the database when you deploy. You do not create a
database account, copy credentials, or configure a connection string.
## Define the schema
Add ordered SQL migrations under `opencomputer/database/migrations`:
```text theme={null}
opencomputer/
└── database/
└── migrations/
├── 001_initial.sql
└── 002_monitor_runs.sql
```
Migration names begin with at least three digits and use lowercase letters,
numbers, underscores, or hyphens after the prefix. For example:
```sql 001_initial.sql theme={null}
CREATE TABLE monitors (
id TEXT PRIMARY KEY,
url TEXT NOT NULL,
query TEXT NOT NULL,
last_checked_at TEXT
);
CREATE TABLE observations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
monitor_id TEXT NOT NULL REFERENCES monitors(id),
observed_at TEXT NOT NULL,
content_hash TEXT NOT NULL,
matched INTEGER NOT NULL DEFAULT 0,
summary TEXT
);
```
Deploy normally:
```bash theme={null}
npx --package @opencomputer/cli opencomputer deploy
```
Before activating the new deployment, OpenComputer applies every pending
migration in filename order. Each migration is transactional and may contain
multiple SQL statements. If a migration fails, the deployment is not
activated.
Applied migrations are immutable. Adding a new migration is safe; editing the
contents of an already-applied filename causes deployment to fail. Use a new
file for each schema change:
```sql 002_monitor_runs.sql theme={null}
ALTER TABLE monitors ADD COLUMN schedule TEXT;
```
> [!NOTE]
>
> Prefer additive, backward-compatible migrations. Existing sessions stay on the
> deployment they started with and can overlap a newer deployment.
## Use the database from an agent
Every project session receives a `database` MCP server automatically. Its
tools run at the OpenComputer edge and do not start or enter a sandbox:
| Tool | Accepted statements | Use |
| - | - | - |
| `database_query` | One `SELECT`, `WITH`, or `EXPLAIN` statement | Read rows and inspect data. |
| `database_execute` | One `INSERT`, `UPDATE`, `DELETE`, or `REPLACE` statement | Change application data. |
Both tools accept `sql` and an optional `parameters` array. Use placeholders
instead of interpolating model-generated values:
```json theme={null}
{
"sql": "INSERT INTO observations (monitor_id, observed_at, content_hash, matched, summary) VALUES (?, ?, ?, ?, ?)",
"parameters": [
"pricing",
"2026-09-17T19:00:00Z",
"8f7c…",
1,
"Enterprise plan appeared"
]
}
```
Schema changes and transaction-control statements are unavailable to tools;
put them in migrations. A call returns columns, rows, rows affected, and a
`truncated` flag. Results are bounded to 200 rows and 256 KiB, so add a `LIMIT`
and paginate large reads.
No hook is required. Describe when the agent should use the database in its
instructions and create the tables it needs with migrations.
## Query from the CLI
Run a read-only query against the linked project's Development database:
```bash theme={null}
opencomputer database query "SELECT * FROM monitors ORDER BY id"
```
Choose Production explicitly, select another project, or bind parameters:
```bash theme={null}
opencomputer database query \
"SELECT * FROM observations WHERE monitor_id = ? LIMIT ?" \
--parameter '"pricing"' \
--parameter 20 \
--environment production \
--project
```
Each `--parameter` is a JSON scalar: a quoted JSON string, finite number,
boolean, or `null`. Add `--json` for the complete machine-readable result.
## Inspect data in the dashboard
Open the project and choose **Database**. The page uses the project's global
Development or Production selector and provides:
* application tables, their schema, and paginated row previews;
* applied migration history; and
* a read-only SQL query box.
The dashboard cannot modify rows or schema. Agent sessions write through
`database_execute`; schema remains source-controlled in the migrations folder.
## Limits
* 100 migrations per deployment manifest
* 256 KiB per migration and 1 MiB across all migrations
* 32 KiB per tool SQL statement
* 100 parameters totaling at most 64 KiB
* 200 returned rows and 256 KiB per result
The database belongs to the project environment, not to an individual agent
or session. Include tenant or user identifiers in your schema when application
data must be isolated within a shared environment.
# Deployments and environments
Source: https://docs.opencomputer.dev/agents/deployments
Understand development, production, immutable deployments, and aliases
A deployment is an immutable build of one agent. An alias gives a stable name
to the deployment clients should run.
New projects have one environment, `default`. Every deploy creates an immutable
deployment and makes it the current one; sessions keep the deployment they
were admitted with. Use separate projects when you need distinct development
and production targets.
Projects created before single-environment mode are legacy projects with two
working environments:
| Environment | Alias | How it changes |
| - | - | - |
| Development | `development` | `npm run deploy -- --watch` publishes source changes |
| Production | `production` | An explicit deploy advances it |
Sessions keep the deployment they started with. Publishing another version
does not change the code underneath an existing session.
## Deploy to production (legacy projects)
From the project directory:
```bash theme={null}
npm run deploy -- --alias production
```
Every deploy first runs `opencomputer doctor`, a local static scan for invalid
connection origins, misplaced tools, and missing secret declarations, and then
prints the project it resolved and the cloud id each local agent deploys as.
Run `opencomputer doctor --json` directly to get structured diagnostics, with
the same resolution under `resolution`, before invoking a deployment agent.
The CLI builds the current source, creates an immutable deployment, and moves
the `production` alias to it. Development remains available for further work.
## Choose a region
By default, an agent's sandbox runs wherever capacity is available, usually in
the United States. To run it elsewhere, set `region` in the agent's
`opencomputer.toml` and deploy again. See [Regions](/agents/regions) for the
allowed values and what a region covers.
## Restrict outbound network
By default, an agent's sandbox can reach any address on the internet. To allow
only the hosts it needs, add an `[egress]` table with an `allow` list to the
agent's `opencomputer.toml` and deploy again. See
[Outbound network](/agents/egress) for what each entry allows and the rules.
To have its traffic leave from fixed IP addresses that others can allowlist,
set `fixed_ips` in the same table. See
[Fixed IP addresses](/agents/egress#fixed-ip-addresses).
## Select an agent and environment
Clients identify an agent with `agent-id@alias`:
```text theme={null}
support@development
support@production
```
Single-environment projects use bare `agent-id` (or `agent-id@default`); older
clients sending `agent-id@development` resolve to the same deployment, while
`agent-id@production` is rejected with `single_environment_project`. Explicit
`--environment` and `--alias` flags are rejected for these projects; `link`
persists the project and `--project` overrides it for one command.
Legacy project dashboards keep the environment selector to change which
deployment you are inspecting. The playground has a separate agent selector when a project
contains more than one agent.
## Review history
Open the project's **Deployments** page to inspect immutable versions, their
aliases, and creation times. Use the deployment ID when correlating a session
with [logs](/agents/logs) or the [debug inspector](/agents/playground).
# Document memory
Source: https://docs.opencomputer.dev/agents/document-memory
Configuration, session bindings, tools and owner APIs
`documentMemory` stores text under a resource, project and environment.
Agents share documents through session bindings. Development and Production
are separate. Notes survive session end, compaction, sandbox replacement and
deployments; memory operations start no sandbox.
See [Memory](/agents/memory) for the concepts, declaration and binding.
## Configuration
```ts theme={null}
import {
defineMemory,
documentMemory,
} from "@opencomputer/agent";
export const notes = defineMemory({
id: "notes",
description: "Workshop requirements and decisions.",
provider: documentMemory({ maxBytes: 8_192 }),
});
```
| Field | Meaning |
| - | - |
| `id` | Shared resource name; see [limits](#limits). |
| `description` | Required, nonempty guidance for the model's tools. |
| `provider` | Defaults to `documentMemory()`. |
| `maxBytes` | Document text limit; default 8,192 UTF-8 bytes. |
The declaration registers a resource; a session binding chooses documents;
`useMemory` selects their projection and tools during render. Declarations
sharing an ID within a deployment must agree on provider and configuration.
* Promotion applies shared limits **before activating code**, including for
sessions on older deployments. If activation fails, applied limits remain;
retry the promotion.
* Lowering a limit never truncates notes or rejects deployment. Oversized
documents remain readable; their next text replacement must fit.
* Changing provider kind requires a new resource and data migration.
## Session bindings
Set `memory.` when creating a session:
```json theme={null}
{
"memory": {
"notes": {
"scope": "document",
"id": "workshop",
"access": "read-write"
}
}
}
```
| Scope and access | Projection | Model tools |
| - | - | - |
| Document, read | Full text | None |
| Document, read-write | Full text | `memory_save` |
| Collection, read | Recent summaries | `memory_list`, `memory_read` |
* For collections, use `"scope": "collection"` and omit `id`. Access defaults
to `read`; collection writes are unsupported.
* Resources must be declared by the deployment and bound documents must exist.
Invalid bindings fail admission.
* Bindings stay fixed. Schedules and webhooks do not configure them.
* One store per project and environment. An application with many users
keeps their data apart with document bindings and application-chosen
document IDs; a collection binding sees every document of the resource, so
it suits a single owner or a coordinating agent, not one user among many.
Calling `useMemory` exposes the binding's tools; omitting it exposes none.
An unbound hook fails before inference. Tools accept a `memory` resource ID,
required when multiple resources **selected by that render** offer the tool.
Descriptions list those eligible resources and their declared guidance.
Session inspection returns a `memory` array. Entries contain `resource`,
`scope`, optional document `id`, `access` and current `writable`. It is false
for read-only bindings, frozen/deleted documents and ended sessions.
Reusing a session-create `Idempotency-Key` requires the same agent (or the
same pinned deployment), environment and bindings; incompatible inputs return
`409 idempotency_conflict`. The session keeps the deployment it started on,
so a redeploy does not change what the key returns. The
[management API](/agents/api#create) page has the full rules.
### Start a session on a new document
A document and a session are separate objects: the document is owner data
with its own lifetime, the binding is that session's admission to it, and
neither is created as a side effect of the other. The SDK and CLI can create
the document and then the session in one call. These are separate requests;
the document remains if session creation fails.
```ts theme={null}
import { OpenComputer } from "@opencomputer/sdk/agents";
const oc = new OpenComputer({ apiKey: process.env.OPENCOMPUTER_API_KEY! });
const { document, session } = await oc.sessions.startOnDocument({
projectId: "",
environment: "development",
agent: "",
resource: "notes",
documentId: "workshop",
document: { title: "Workshop notes" },
idempotencyKey: "topic/workshop/1",
});
// document.created and session.created are false when they already existed.
```
* The document is created with `If-None-Match: *`. An existing document is
left as it is; `document` is ignored then. A deleted id is reserved and
fails before any session is created.
* The session is created with an `Idempotency-Key` derived from
`idempotencyKey`. The same key with the same agent, environment and
bindings returns the existing session; anything else under that key
fails with code `idempotency_key_reused`. `access` (default `read-write`),
`memory` for further bindings and `source` are passed through.
The helper addresses the agent by its environment alias; the platform
records the deployment the session started on, so a retry after a redeploy
returns the same session. Do not change the key to recover a lost response:
that would create another session.
The CLI does the same for a Development session:
```bash theme={null}
opencomputer session create --memory notes=workshop --create-document --idempotency-key topic/workshop/1
```
`--memory =[:read|read-write]` binds a document
(read-write by default); `--memory ` binds a collection.
`--create-document` creates each bound document that does not exist yet,
titled after its id, and reports it as created or existing. Add a prompt to
run the first turn at once, and `--json` for the ids.
Over HTTP, send `PUT .../documents/` with `If-None-Match: *`, treat `412`
as "exists" (then `GET` it; `404` means the id was deleted), and `POST
/api/managed-agents/sessions` with the document binding,
`agentId: @` and `Idempotency-Key`; `201` created the
session, `200` returned the one that key had already created. Keep that
envelope unchanged for retries. See [session creation](/agents/api#create)
for the body and the
[Serverless Agents client reference](/reference/typescript-sdk/agents#start-a-session-on-a-memory-document).
## Reading
The hook is synchronous; pass the definition or its ID:
```ts theme={null}
import type { MemorySource } from "@opencomputer/agent";
declare function useMemory(
memory: string | { readonly id: string },
): {
readonly text: string;
readonly sources: readonly MemorySource[];
readonly writable: boolean;
};
```
* `text`: full document text or a collection overview.
* `sources`: included documents, each with `id`, `title`, `revision` and
`updatedAt`. Empty documents return `text: ""` and still have a source.
* `writable`: permission **at recall**, always false for collections. It
neither reserves permission nor changes the binding's tools. Later edits,
freezes, deletion or session end can still prevent a save.
Every bound resource is recalled before render; any failure blocks inference,
even if that render would omit the hook. The next render reads current notes.
Compaction does not alter stored memory or automatically extract new notes.
### Collections
The overview contains recent entries in this format:
```text theme={null}
workshop | Workshop notes | Node.js 22. | 2026-09-10
```
Fields are ID, title, summary and update date. Full text requires
`memory_read`; `memory_list` reaches documents beyond the overview. Model
calls, with a single selected resource:
```js theme={null}
memory_read({ id: "workshop" })
memory_list({})
memory_list({ cursor: "" })
```
Read results contain `id`, `title`, `text`, `summary`, `revision`, `updatedAt`.
Lists return `{ documents, nextCursor }`; entries omit `text`. Pass the opaque
cursor with the same resource/environment; null ends pagination. These tools
require a collection binding. Missing documents return status `rejected`,
reason `not_found`; invalid cursors return reason `invalid_cursor`.
## Saving
`memory_save` replaces the bound document:
```json theme={null}
{
"memory": "notes",
"text": "Exercises must run on Node.js 22.",
"summary": "Workshop runtime requirements."
}
```
`text` is required. Omitting `summary` preserves it; `""` clears it. Agents
cannot change IDs, titles or write policy. Results have these shapes:
```ts theme={null}
type SaveResult =
| { status: "saved"; revision: string; bytes: number }
| { status: "conflict"; text: string; summary: string }
| {
status: "rejected";
reason: "agent_writes_disabled" | "not_found" | "session_closed";
}
| {
status: "rejected";
reason: "too_large";
field: "text" | "summary";
bytes: number;
maxBytes: number;
}
| {
status: "rejected";
reason: "not_bound" | "read_only";
bound: string[];
};
```
`saved` commits independently of turn completion. `conflict` carries current
content for reconciliation. Rejections identify frozen or deleted documents,
a session whose end already revoked its writes, a field above its limit, a
resource the render did not select, or a read-only binding. `bound` lists
the resources the save could have targeted. These outcomes let the model continue; malformed arguments
and operational failures are tool errors.
### Conflicts and retries
The host supplies the revision from the originating render. Every save and
retry within that model response uses it: another writer's edit causes a
conflict, as does a second save after the first succeeds. Reconcile in the
next model step; there is no automatic merge.
A committed save whose reply was lost can conflict on retry. Read current
content before writing again. Separately, **model-generation retries can
render again using newer memory**; they do not preserve the same projection.
Observed saves produce `memory.saved` [session events](/agents/events#memory)
with `resource`, `documentId`, `revision`, `bytes`. Delivery is best-effort: a
write can commit without a reply or event. Refresh documents when attaching,
reconnecting and completing work.
## Owner access
The project's **Memory** page shows documents, last writers and update times,
including resources retired from active code. Owner edits use revision checks.
Keep owner credentials in trusted application code.
Install the CLI, then [log in and link your project](/agents/quickstart):
```bash theme={null}
npm install -g @opencomputer/cli
```
Commands default to Development; add `--environment production` for
production.
List metadata:
```bash theme={null}
opencomputer memory list notes
```
Read a document:
```bash theme={null}
opencomputer memory show notes workshop
```
Create empty notes:
```bash theme={null}
opencomputer memory create notes workshop --title "Workshop"
```
Open existing text in your editor:
```bash theme={null}
opencomputer memory edit notes workshop
```
Create and edit accept `--text-file `, `--text-stdin`, and
`--summary `. Create also accepts `--frozen`. Add `--json` for
structured output: list follows all pages and returns `{ documents }`; show
returns the [document object](#documents).
### Disable agent writes
Freeze:
```bash theme={null}
opencomputer memory freeze notes workshop
```
Re-enable writes:
```bash theme={null}
opencomputer memory unfreeze notes workshop
```
Freeze sets `agentWrites: "disabled"`; after success, no later agent save can
commit. Earlier saves remain; reads and owner edits continue. Unfreeze enables
active bindings without reviving ended sessions or cancelling work.
Successful session end revokes its document-memory access. A failed or timed
out end has an uncertain outcome; freeze the document to block all agent
writes while resolving it. [Remote providers](/agents/memory-providers) have
a separate completion contract.
### Export saved notes
```bash theme={null}
opencomputer memory export --out ./memory
```
Export includes retired resources; repeat `--resource ` to restrict it.
Each `//.json` contains current document fields. Reads are
individual: this is not a consistent snapshot or revision/session backup.
Credentials, conversation history and sandbox files are excluded.
### Delete a document
```bash theme={null}
opencomputer memory remove notes workshop
```
Deletion removes title, text and summary but reserves the ID against delayed
recreation. Collections omit it; existing document bindings fail their next
recall, and pending saves return `not_found`. Copies in history, tool results
and exports remain. Use a new ID for new notes.
## Management API
Use `x-api-key: ` on `https://app.opencomputer.dev`, as for the rest
of the [management API](/agents/api). Every route requires
`environment=development` or `environment=production` as a query parameter.
Below, `
` is the project ID and `` the resource ID.
### Resource inventory
```text theme={null}
GET /api/managed-agents/projects/
/memory
```
```json theme={null}
{
"resources": [
{
"id": "notes",
"provider": {
"kind": "document",
"maxBytes": 8192
},
"declared": true,
"documents": 2
}
]
}
```
* `provider`: stored kind and current limit.
* `declared`: whether any active deployment in the environment declares it.
* `documents`: count of non-deleted documents.
Retired resources remain discoverable, editable and exportable. Removing
declarations neither revokes existing bindings nor deletes notes. New sessions
need a deployment declaring their resources.
Renaming an ID requires migration; reusing it addresses the same stored data.
### Documents
Paths are relative to this base; append `?environment=development`:
```text theme={null}
/api/managed-agents/projects/
/memory//documents
```
| Operation | Method/path | Success |
| - | - | - |
| List metadata | `GET` | `200` |
| Read | `GET /` | `200` |
| Create | `PUT /` | `201` |
| Replace text/summary | `PUT /` | `200` |
| Change title/policy | `PATCH /` | `200` |
| Delete | `DELETE /` | `204`, empty body |
Create requires `If-None-Match: *` and a never-used ID. Other mutations require
`If-Match: ""`; checks and writes happen together. JSON bodies:
* **Create:** required `title`; optional `text`, `summary` default to `""`;
`agentWrites` defaults to `"enabled"`.
* **Replace:** required `text`; omitted `summary` is preserved.
* **Patch:** `title`, `agentWrites`, or both. Omitted fields stay unchanged.
`agentWrites` accepts `"enabled"` or `"disabled"`. Unknown fields are rejected.
Read, create, replace and patch return the document and its quoted `ETag`:
```json theme={null}
{
"id": "workshop",
"title": "Workshop notes",
"text": "Exercises must run on Node.js 22.",
"summary": "Workshop runtime requirements.",
"agentWrites": "enabled",
"revision": "",
"bytes": 33,
"maxBytes": 8192,
"updatedAt": "2026-09-10T12:00:00.000Z",
"writer": { "kind": "owner" }
}
```
`bytes` counts UTF-8 text bytes; `maxBytes` is the current limit. The platform
sets `writer` to `{ kind: "owner" }` or `{ kind: "agent", sessionId }`.
Revision is opaque: retain `ETag` verbatim for `If-Match`. Delete returns no
new document ETag.
List returns `{ documents, nextCursor }`, omitting `text` from entries. Pass
`nextCursor` as the `cursor` query parameter until null. Order is descending
update time, then ascending ID. Pagination is live; concurrent edits can move
documents between pages. Re-list for a complete current inventory.
### Errors
Error bodies are `{ error: { code, message } }`:
| Status | Code | Meaning |
| - | - | - |
| `400` | `invalid_request` | Invalid input, environment or conditions |
| `404` | `not_found` / `project_not_found` | Missing/deleted target |
| `412` | `precondition_failed` | Stale revision or already-used create ID |
| `413` | `memory_limit_exceeded` | Field size limit |
| `428` | `precondition_required` | Missing condition header |
Authentication/authorization failures return `401`/`403`. On `412`, read the
latest document and reconcile.
Network/server errors can leave mutations committed without acknowledgement.
Read current state before retrying; conditional writes protect newer content
but do not replay the original response.
## Limits
| Item | Limit |
| - | - |
| Resource ID | 1–128 lowercase letters/digits, single hyphen separators |
| Document ID | 1–128 ASCII letters, digits, `-`, `_` |
| Document text | `maxBytes`: integer 1–16,384; default 8,192 UTF-8 bytes |
| Title | 1–240 UTF-8 bytes |
| Summary | 0–240 UTF-8 bytes |
| Mutation JSON body | 64 KiB |
| Bindings per session | 8 |
| Collection overview | 50 whole entries within 16 KiB |
| List page | 50 documents |
Field limits are separate. These are not token budgets: memory shares the
model context with instructions, history and tools, without silent truncation.
# Outbound network
Source: https://docs.opencomputer.dev/agents/egress
Allow an agent's sandbox to reach only the hosts it needs
By default, an agent's sandbox can reach any address on the internet. To allow
only the hosts the agent needs, add an `[egress]` table to the agent's
`opencomputer.toml` and deploy again:
```toml theme={null}
schema = 1
id = "support"
name = "Support"
[egress]
allow = [
"api.github.com", # exact domain
"example.com", # exact domain
"*.example.com", # wildcard: every subdomain, not example.com itself
"203.0.113.10", # IP address
"10.20.0.0/16", # CIDR
]
```
Every process in the sandbox is held to the list: commands, `sandbox_exec`,
background jobs, and anything they start. Connections to anything else are
refused.
## What each entry allows
| Entry | Allows |
| - | - |
| `"api.github.com"` | HTTPS (TLS on port 443) to that host |
| `"*.example.com"` | HTTPS to any subdomain, at any depth, but not `example.com` itself |
| `"203.0.113.10"`, `"2001:db8::1"` | any protocol and port to that address |
| `"10.20.0.0/16"`, `"2001:db8::/32"` | any protocol and port to that range |
* **A wildcard does not cover its own domain.** `"*.example.com"` allows
`api.example.com` and `a.b.example.com`, not `example.com`. To reach both,
list `"example.com"` and `"*.example.com"`.
* **Domains cover HTTPS on port 443 only.** Plain `http://`, other ports, and
non-TLS protocols to a domain are refused. To reach those, list the
destination's IP address or CIDR instead.
* **Domains are matched on the TLS server name.** The connection goes to the
allowed host whatever IP address the client picks, so pointing an allowed
name at another address does not reach that address.
* **DNS lookups are not filtered.** Names outside the list still resolve; the
connection is what is refused. Resolvers other than the sandbox's own are
blocked.
* **Platform hosts are always allowed.** The sandbox can always reach
OpenComputer and the storage it loads your agent from, so you never list
them.
`allow = []` (or an `[egress]` table with no `allow`) allows nothing beyond the
platform hosts. Without an `[egress]` table the network stays open.
## Rules
* Up to 64 domains and 64 IP addresses or CIDRs.
* Domains take no scheme, port or path. `"https://api.github.com"` and
`"api.github.com:443"` are rejected. A bare `"*"` is rejected; remove the
`[egress]` table instead.
* A bare IP address means that single address (`/32` or `/128`).
An invalid entry stops the deploy before anything is uploaded:
```text theme={null}
opencomputer.toml [egress] allow entry "https://api.github.com" is not a domain ("api.github.com", "*.example.com"), IP address or CIDR; leave out schemes, ports and paths
```
The API rejects it too, with `400 invalid_egress`, if you call
`POST /v1/deployments` directly. There the allowlist is
`"egress": { "domains": [...], "cidrs": [...] }`; the CLI splits `allow` into
these two lists for you.
## Fixed IP addresses
To have the sandbox's traffic leave from a fixed set of IP addresses, for
example so a customer can allowlist your agent, set `fixed_ips` to a region:
```toml theme={null}
[egress]
fixed_ips = "au"
```
All of the sandbox's outbound traffic then leaves from that region's
addresses. A region can have more than one; successive tool calls take turns
between them, so allowlist every address the region lists. The regions and
their addresses are public:
```bash theme={null}
curl https://managedagents.opencomputer.dev/v1/egress-ips
```
```json theme={null}
{ "regions": { "au": { "ipv4": ["209.71.97.179", "209.71.110.167"] }, ... } }
```
* `fixed_ips` works with or without `allow`. With both, the sandbox reaches
only the allowed hosts, and reaches them from the fixed addresses.
* If the region's addresses are unreachable, the sandbox has no network rather
than falling back to other addresses.
* The region of `fixed_ips` is independent of the agent's `region`; pick the
same one to keep latency down.
An unknown region stops the deploy, and the API answers `400 invalid_fixed_ips`
to `"fixedIps": ""` on `POST /v1/deployments`.
## Changing the list
A change applies to sandboxes started after the deploy. A sandbox that is
already running keeps the list it started with until it stops. Each agent in a
project has its own `opencomputer.toml`, so agents in one project can have
different lists.
The list covers the agent's sandbox. Model calls, and MCP servers the agent
connects to, are made by OpenComputer outside the sandbox and are not held to
it.
# Session events
Source: https://docs.opencomputer.dev/agents/events
The durable event log every session keeps and every consumer reads
Every session appends what happens to one durable log: the turns it
accepted, the text the model produced, the tools it called, the memory it
saved and the runtime that ran it. The log is what `opencomputer sessions
tail --json` prints, what [`useAgent`](/agents/react) reduces to messages,
what the dashboard's session view shows, and what
[`GET /sessions//events`](/agents/api#events) returns.
## Shape and ordering
Each event is one JSON object:
| Field | Meaning |
| - | - |
| `seq` | Position in the log, starting at 1 and increasing with every event |
| `id` | A unique event ID |
| `timestamp` | When the event was recorded |
| `sessionId` | The session |
| `turnId` | The turn the event belongs to; absent on session-level events |
| `type` | One of the types below |
| `data` | Fields specific to the type |
`seq` is the cursor. A read with `after=` returns events with a greater
`seq`, in ascending order, up to 500 at a time; repeat from the last `seq`
you received until a page is empty, and keep polling from there to follow a
live session. Because the log is durable and `seq` only grows, a consumer
that stops can resume from its cursor without missing anything, and a page
that overlaps one already read is harmless: apply events whose `seq` is
greater than what you have applied. The React hook and the CLI work this
way.
Events at or below the cursor never change. New types can appear; treat an
unknown type as informational and keep reading.
## Event types
### Session lifecycle
| Type | When | `data` |
| - | - | - |
| `session.created` | The session exists. Always the first event. | `agentId`, `deploymentId` |
| `session.status_changed` | The session moved between `new`, `connecting`, `idle`, `running`, `stopping`, `waiting_runtime`, `suspending`, `suspended`, `resuming`, `failed` and `ended` | `from`, `to` |
| `session.ended` | The session accepts no more turns; queued and running turns were marked cancelled. Memory revocation and remote cleanup may still be pending. | none |
| `session.failed` | The session cannot continue | `code`, `message`: a [public failure](#public-failures) |
When the session was created with an [external
reference](/agents/api#external-references), every `session.*` event's
`data` also carries it as `externalReference`.
`stopping` follows an [interrupt](/agents/api#end-and-interrupt) or an
interrupt-mode turn: the session stays there until the stopped turn's
commands are confirmed stopped or its computer is terminated, then `idle`
follows and the next turn can start. See the API's compatibility note for
sessions that cancel immediately instead.
`session.ended` is not a command-settlement or memory-revocation receipt.
The [end response](/agents/api#end-and-interrupt) confirms memory revocation;
remote cleanup continues in the background.
### Turns
| Type | When | `data` |
| - | - | - |
| `message.received` | A turn was accepted; its input is recorded before anything runs | `input`: the user text; `mode`: `queue`, `steer` or `interrupt`; `payload`: the structured value the turn was sent with, when there was one |
| `turn.queued` | The turn waits for earlier turns | `mode` |
| `turn.steered` | The input was delivered into the running turn | `activeTurnId` |
| `turn.interrupted` | This turn requested interruption of the running turns | `interruptedTurnIds` |
| `turn.started` | The runtime began the turn | none |
| `turn.completed` | The turn finished and the session is idle again | none, or `outcome: "question"` and `questionId` when the turn ended by [asking](#questions) |
| `turn.failed` | The turn stopped with an error | `code`, `message`, and `model` or `tool` when named: a [public failure](#public-failures) |
| `turn.cancelled` | The turn was cancelled by an interrupt or session end, held behind a question, or stopped from Linear | `reason`: `interrupted`, `session_ended`, `held` or `stopped`; optional `replacementTurnId`, `settledAfterMs`, `operationsSettled`, `computerTerminated`; `questionId` with `held`; `discarded: true` on a queued turn a stop discarded |
For an interrupt that waits for commands, `settledAfterMs` records the wait,
`operationsSettled` counts the settled commands, and `computerTerminated`
says whether stopping required terminating the computer. Cancellation with
`reason: "session_ended"` has no settlement fields: it records the session's
decision to end, before remote cleanup finishes. Immediate-cancellation
sessions also omit these fields; see [End and interrupt](/agents/api#end-and-interrupt).
A turn that was queued behind a turn that asked a question is cancelled
with `reason: "held"` and the `questionId`: its input did not run on its own
and is delivered with the answer as `steering`.
#### Public failures
A failure's `data` is a stable `code`, a fixed `message` for that code, and
at most one parameter. The runtime's own error text is never sent; a failure
no rule recognizes is `agent_failed`.
A model call that fails in flight may be retried inside the turn before
the turn fails. The runtime retries provider errors, rate limits,
incomplete response streams and transport failures that interrupt a
started response on its own schedule, per model call; the host adds one
retry for a transport failure or an invalid response the runtime would
not retry on its own. The agent's own calls are covered; the runtime's
auxiliary calls (compacting the conversation, titling it) are not
retried. Each retry appears in the log as a `runtime.log` milestone with
`phase: "model_retry"`, the attempt and the failure's class (for example
`provider.transport`), never the provider's own text. A rejection is not
retried: credentials, quota, content policy and an invalid request are
`model_rejected` at once. A `model_stream_failed` message names its retry
only when one happened.
| `code` | Meaning | Parameter |
| - | - | - |
| `interrupted` | The turn was stopped before it finished | |
| `session_ended` | The session ended while the turn ran | |
| `runtime_lost` | The runtime stopped responding and the turn was abandoned | |
| `runtime_failed` | The runtime failed before the turn finished | |
| `deployment_invalid` | The runtime could not load the deployment | |
| `model_unavailable` | The requested model is not available to this agent | `model`: the requested model id |
| `model_rejected` | The model provider rejected the request: credentials, rate limit or quota | |
| `context_too_long` | The conversation exceeds the model's context window | |
| `tool_failed` | A tool failed | `tool`: the tool id, when the runtime named it |
| `sandbox_timeout` | A sandbox command did not finish in time | |
| `sandbox_failed` | The sandbox could not run the turn | |
| `model_stream_failed` | The model call failed before it finished, or its response stream broke; when it was retried, the retry failed too | `model`: the model id, when known |
| `agent_failed` | Any other failure | |
### Questions
| Type | When | `data` |
| - | - | - |
| `question.asked` | The agent called [`ask`](/agents/tools#ask-a-question); the asking turn completes with `outcome: "question"` | `questionId`, `text`, `options`: `{ label, value }` choices, empty for free text |
| `question.answered` | A turn answering the question was admitted | `questionId`, `answer`: `{ questionId, text, value? }` |
| `question.closed` | The question closed without an answer | `questionId`, `reason`: `stopped`, `dismissed`, `ended` or `undeliverable` |
| `message.held` | An input sent without `answers` was held behind the open question | `heldId`, `questionId`, `input`, `payload?`, `receivedAt` |
| `message.delivered` | A held input reached the agent | `heldId`, `answerTurnId` (the turn that carried it), `as`: `steering`, `answer` or `turn` |
| `message.discarded` | A held input will not run | `heldId`, `reason`: `stopped` or `ended` |
`turn.completed` with `outcome: "question"` is the turn's terminal event, so a
consumer that knows only completed turns reads an asking turn correctly, and
[outcome subscriptions](/agents/api#event-subscriptions) deliver it as a
completed turn. The open question is also on the session as
[`question`](/agents/sessions#questions), where each closure `reason` is
explained. A held input is recorded once, however often its key is repeated,
so a client can rebuild held messages after a reload.
### Messages
| Type | When | `data` |
| - | - | - |
| `message.delta` | The model produced a fragment of its reply | `text`: the fragment; concatenate deltas of one turn |
| `message.completed` | The reply is complete | `text`: the whole reply |
| `reasoning.delta` | The model produced a fragment of reasoning, when the model exposes it | `text` |
| `reasoning.completed` | The reasoning is complete | `text` |
### Tools
| Type | When | `data` |
| - | - | - |
| `tool.started` | The model called a tool | `tool`: its name; `callId`; `title`; `input`: the arguments as a JSON value |
| `tool.progress` | The tool reported progress | runtime-defined |
| `tool.completed` | The tool returned | `tool`, `callId`, `title`, `output`: what the tool returned as a JSON value; `result: true` when the output became the session's result |
| `tool.failed` | The tool raised an error, or its turn ended before it completed | `tool`, `callId`, `title`, `message`; `settledBy` when the turn's end settled the call: `turn.completed`, `turn.failed` or `turn.cancelled` |
The exact fields come from the runtime's tool record; read them as optional
and key a tool call on `callId` when it is present. The memory tools
(`memory_save`, `memory_read`, `memory_list`) appear here like any other
tool; the save itself is reported separately.
`input` and `output` are JSON values, never JSON text: an object comes as an
object, and a consumer reads it without parsing. Tool events recorded before
these fields were introduced keep the shape they were recorded with, so the
React hook's [`turns`](/agents/react#hook-reference) shows those calls unnamed and
without output; `messages` is unaffected.
A successful call of the agent's [result tool](/agents/tools#the-session-result)
is recorded with `result: true`; its `output` is the validated value, the
same JSON value `GET /sessions/` returns as `result.data`. The session
writes this event when it commits the result, before the model sees the call
succeed, and there is one per call.
Every tool call ends in the log. The runtime reports a `tool.completed` or
`tool.failed` per call; when a turn ends with a call still open, whether
the turn completed, failed or was cancelled, the session records a
`tool.failed` for that call ahead of the terminal turn event, in the same
write, with `settledBy` naming the terminal event (`turn.completed`,
`turn.failed` or `turn.cancelled`) and a `message` saying the turn ended
first. A consumer therefore never sees a settled turn with a running call;
one that keys rows on `callId` needs no rule of its own for the turn's end.
This settles the call in the log; it does not strengthen the command-stop
guarantees of the terminal turn event.
### Memory
| Type | When | `data` |
| - | - | - |
| `memory.saved` | A `memory_save` the session observed succeeding | `resource`, `documentId`, `revision`, `bytes` |
Delivery is best-effort. A save commits independently of the event: a
runtime that loses its connection can commit without ever reporting it, and
the same event can be reported once or twice after a reconnect. Treat the
event as a hint to re-read the document, and also re-read when attaching,
reconnecting and completing work. See
[Document memory](/agents/document-memory#saving).
### Model and usage
| Type | When | `data` |
| - | - | - |
| `model.route_resolved` | A model call is about to be sent | `providerCallId`; `requested` and `effective` as `{ provider, model }`; `runtime`; `access` |
| `model.access_fallback` | A call planned on a connected model account fell back to managed access | `providerCallId`, `requested`, `from`, `reason` |
| `usage.recorded` | A model call finished | `provider`, `model`, `inputTokens`, `outputTokens`, `reasoningTokens`, `cachedTokens`, `cacheWriteTokens`, `costUsd`, `payer` |
### Outbound requests
| Type | When | `data` |
| - | - | - |
| `egress.request` | A [declared connection](/agents/secrets) sent a request | `connectionId`, `method`, `path` |
| `egress.response` | The destination answered | `connectionId`, `method`, `path`, `status`, `durationMs` |
| `egress.failed` | The request did not complete | `connectionId`, `method`, `path`, `message` |
### Runtime
| Type | When | `data` |
| - | - | - |
| `runtime.connected` | An agent runtime attached to the session | none |
| `runtime.disconnected` | The runtime connection closed; running turns return to the queue to await a runtime | none |
| `runtime.suspended` | The runtime was suspended between turns | none |
| `runtime.resumed` | The runtime was resumed | none |
| `runtime.log` | A line of runtime output or a platform milestone | `level`, `stream`, `message` for output; `phase` and `message` for milestones such as the runtime starting |
A runtime disconnect does not prove that a command stopped or that its
effects were undone. Follow the subsequent turn events to learn whether
work continued or failed; see [Durability and recovery](/agents/sessions#durability-and-recovery).
### Agent renders
| Type | When | `data` |
| - | - | - |
| `agent.rendered` | Your agent function was rendered for a model step | `renderId`, `instructions`, `model`, `enabledTools`, `enabledMcpServers`, `requiredConnections`, `input`, `tools`, `renderedAt` |
One turn can carry several renders, one per model step. The debug inspector
in the [playground](/agents/playground) shows the same data.
## Reading the log
From the CLI, with one NDJSON record per event:
```bash theme={null}
opencomputer sessions tail --after 0 --json
```
From an application, poll the [events route](/agents/api#events) or attach
[`useAgent`](/agents/react), which turns `message.received`,
`message.delta` and `message.completed` into messages, `turn.*` and
`session.*` into `isRunning` and `ended`, and `memory.saved` into
`memorySaves`; every event, these included, also reaches `onEvent`.
# Feature flag hygiene
Source: https://docs.opencomputer.dev/agents/examples/feature-flag-hygiene
Inspect Unleash flags, find safe cleanup candidates, and open focused pull requests
The feature flag hygiene example finds old Unleash release flags that remain in
a repository and prepares one small cleanup pull request per eligible flag. It
combines an interactive agent, remote MCP servers, code-defined tools, and a
schedule in one project.
> **[Deploy to OpenComputer](https://app.opencomputer.dev/new?repository-url=https%3A%2F%2Fgithub.com%2Fdiggerhq%2Fopencomputer-example-unleash)**
>
> Configure the GitHub and Unleash credentials, then deploy the complete agent
> and schedule to Development.
[Browse the source on GitHub](https://github.com/diggerhq/opencomputer-example-unleash).
## How it works
```mermaid theme={null}
flowchart LR
Start[User message or weekday schedule]
Agent[Feature flag hygiene agent]
GitHub[GitHub repository and ownership]
Unleash[Unleash MCP production state]
Check{Safe to remove?}
PR[One cleanup PR per flag]
Report[Explain why the flag was skipped]
Start --> Agent
Agent --> GitHub
Agent --> Unleash
GitHub --> Check
Unleash --> Check
Check -->|yes| PR
Check -->|no| Report
```
For each exact feature-flag reference found in executable code, the agent:
1. inspects the flag's production state through the Unleash MCP server;
2. checks whether it has been live for the configured minimum age;
3. determines the behavior that should survive removal;
4. finds an owner through `CODEOWNERS` or recent file history;
5. runs the narrowest relevant tests;
6. opens one focused pull request per eligible flag and requests the identified
owner as a reviewer.
The agent does not disable, archive, or delete flags in Unleash. Flag retirement
remains a separate action after the cleanup pull request has been reviewed and
deployed.
## Select the capabilities
The agent function attaches the model and capabilities needed by the managed
harness:
```tsx theme={null}
export default function Agent() {
const input = useInput();
useModel("anthropic/claude-sonnet-4.6");
useMcpServer(unleashMcp);
useMcpServer(githubPatMcp);
useTool(cloneRepository);
useTool(findFileOwners);
useTool(openCleanupPullRequest);
// Return the workflow instructions for this render.
}
```
The Unleash MCP server supplies feature-flag state. The GitHub MCP server and
code-defined tools supply repository search, an authenticated source snapshot,
ownership evidence, branch creation, file updates, pull requests, and reviewer
selection.
Both credentials are managed by OpenComputer. They are not placed in agent
source, prompts, commands, tool input, or repository URLs.
## Skip unsafe candidates
The agent refuses to open a pull request when a flag:
* is younger than the minimum-age threshold;
* is not enabled in production;
* appears only in documentation;
* has ambiguous surviving behavior;
* has failing tests;
* has no evidence for an owner; or
* already has an open cleanup branch or pull request.
The screenshot below shows a real interactive run in which all four flags were
skipped. Three were enabled but too young, while one was not enabled in
production. The session reports the evidence instead of making a change.

## Run it on a schedule
The example includes a code-defined weekday schedule:
```ts theme={null}
import { defineSchedule } from "@opencomputer/agent";
export default defineSchedule({
id: "weekday-hygiene",
cron: "0 9 * * 1-5",
timezone: "America/Los_Angeles",
enabled: ["production"],
overlap: "skip",
dispatch: {
text: "Run the configured feature-flag hygiene review.",
payload: {
mode: "async",
repository: "diggerhq/opencomputer-example-unleash",
projectId: "feature-example-test",
productionEnvironment: "production",
minimumAgeDays: 10,
githubAuth: "pat",
dryRun: true,
},
},
});
```
Development discovers the schedule but keeps it manual-only by default. Use
**Run now** to test the exact dispatch safely. Production runs it at 9:00 AM
Pacific on weekdays and skips an occurrence when an earlier run is still
active.
The payload's `mode` selects unattended behavior. The transport provenance in
`input.source` remains available for observability, but it is not used as the
agent's business-mode switch.
## Try the fixture safely
After cloning the repository and configuring the documented GitHub and Unleash
development secrets:
```bash theme={null}
npm install
npm run deploy -- --watch
```
Start with the included fixture and a dry run:
```bash theme={null}
npm run session -- "Analyze repository diggerhq/opencomputer-example-unleash using Unleash project feature-example-test and production environment. Use minimumAgeDays 0, GitHub PAT mode, and dryRun true."
```
The repository README documents the required token permissions, production
deployment, fixture tests, and the deliberate step from dry-run analysis to
pull-request creation.
# GTM engineer
Source: https://docs.opencomputer.dev/agents/examples/gtm-engineer
A scheduled go-to-market agent that researches opportunities with Exa and keeps every action in dry-run mode
The GTM engineer example is an agent-only go-to-market (GTM) demo built with
OpenComputer and Exa. It researches social opportunities, social-content and
blog ideas, public-evidence outreach prospects, and campaign strategy from the
OpenComputer debug playground or code-defined schedules.
The example is intentionally read-only. It can research and draft selected
ideas, but it has no tools for publishing, messaging, CRM updates, enrollment,
or writing analytics data.
## How it works
1. A user message or schedule starts a durable OpenComputer session with a
workflow payload.
2. The agent uses its `exa_search` tool to find current public evidence for the
requested workflow.
3. The declared Exa connection sends requests with the write-only
`EXA_API_KEY` without adding it to agent source.
4. The agent returns a ranked shortlist with source URLs and waits for a user
to select items before it drafts anything.
5. Selection authorizes drafting only; the agent cannot publish or contact
anyone.
The repository includes five schedules for social discovery, content
research, blog research, outreach research, and strategy review. Development
schedules are manual-only, while production schedules recur at their defined
cadences and skip overlapping runs.
## Start with the example
> **[Deploy to OpenComputer](https://app.opencomputer.dev/new?repository-url=https%3A%2F%2Fgithub.com%2Fdiggerhq%2Fopencomputer-example-gtm)**
>
> Configure research credentials and deploy the agent and schedules to
> Development.
[Browse the source on GitHub](https://github.com/diggerhq/opencomputer-example-gtm).
The repository README covers installation, setting the Exa secret, testing in
the debug playground, running schedule payloads, and deploying the agent.
# Pull Request Reviewer
Source: https://docs.opencomputer.dev/agents/examples/pr-review
Fetch a PR through a managed GitHub connection, review it, and post inline comments on request
The [Pull Request Reviewer example](https://github.com/diggerhq/opencomputer-example-pr-review)
reviews a GitHub pull request on demand. The agent fetches the PR metadata and
diff through a declared GitHub connection, reviews the change for correctness,
and returns the review as session output. When the request explicitly asks,
it posts a `COMMENT` review: a verdict plus findings, with inline comments
anchored to diff lines:

## Structure
**The agent function** (`opencomputer/agents/pr-review/agent.ts`) is a
synchronous render, called before every model step. It reads the current
input and selects the instructions and tool set for that step. The write
tool is attached only when the request asks for a live post:
```tsx theme={null}
export default function Agent() {
const input = useInput();
const live = /\bpost\b.*\breview\b/i.test(input.text ?? "");
useModel("anthropic/claude-sonnet-4.6");
useTool(getPullRequest);
useTool(getDiff);
if (live) {
useTool(postReview);
}
return live
? "…review, then post with post_review and report the review URL."
: "…review as a dry run. Do not claim to have posted anything.";
}
```
There is no separate permission layer: dry-run-by-default is one `if`
statement, and a tool the render does not select does not exist for that
model step.
**The tools module** (`opencomputer/agents/pr-review/tools/github-tools.ts`)
declares one HTTP connection and three typed tools that use it.
`get_pull_request` and `get_diff` read; `post_review` writes one `COMMENT`
review and falls back to summary-only when GitHub rejects an inline anchor:
```tsx theme={null}
export const github = defineConnection({
id: "github-api",
origin: "https://api.github.com",
methods: ["GET", "POST"],
pathPrefix: "/repos/",
headers: {
Authorization: bearer(useSecret("GITHUB_TOKEN")),
"User-Agent": "opencomputer-pr-review-agent",
},
});
```
The connection defines all outbound access: tools call
`github.fetch("/repos/…")`, and the platform validates origin, method, and
path prefix before attaching the token at its outbound edge. The token never
enters the agent runtime, so a prompt-injected model step cannot read or
exfiltrate it. See [Secrets and runtime variables](/agents/secrets).
**The managed runtime** provides the rest: the durable session and turn
queue, the model/tool loop that calls the render before each step, and
deployments. `npm run deploy -- --watch` builds an immutable
content-addressed deployment on every save and advances the Development
alias; sessions stay pinned to the deployment they started on.
```mermaid theme={null}
flowchart LR
Input[Session, playground, or webhook input]
Render[Render: instructions + tools for this input]
Read[get_pull_request get_diff]
Conn[Managed connection api.github.com]
Dry[Review as session output]
Post[post_review: COMMENT + inline]
Input --> Render
Render --> Read
Read --> Conn
Render -->|default| Dry
Render -->|"post the review"| Post
Post --> Conn
```
## Run it
> **[Deploy to OpenComputer](https://app.opencomputer.dev/new?repository-url=https%3A%2F%2Fgithub.com%2Fdiggerhq%2Fopencomputer-example-pr-review)**
>
> Configure the GitHub credential and deploy the agent to Development.
[Browse the source on GitHub](https://github.com/diggerhq/opencomputer-example-pr-review).
```bash theme={null}
git clone https://github.com/diggerhq/opencomputer-example-pr-review.git
cd opencomputer-example-pr-review
npm install
npm run opencomputer -- login
npm run deploy -- --watch
```
Keep the watch command running while testing; it deploys changes to the
Development environment.
## Configure the GitHub token
Create a fine-grained personal access token scoped to the repositories the
agent should review, with **Pull requests: read and write** and
**Contents: read**. Store it as a managed secret:
```bash theme={null}
npm run opencomputer -- secrets set GITHUB_TOKEN
```
The agent's repository access equals the token's grant: it can read and
review only the repositories selected when the token was created.
## Review a pull request
```bash theme={null}
npm run session -- "Review acme/widgets#42"
```
The dry-run review is a verdict paragraph plus numbered findings anchored to
files and lines from the diff. Nothing is posted.
To post the review:
```bash theme={null}
npm run session -- "Review acme/widgets#42 and post the review on the PR."
```
The agent submits one `COMMENT` review — the verdict and findings as the
body, plus inline comments for findings it can anchor to new-side diff
lines. It never approves or requests changes.
## Trigger from another system
```bash theme={null}
npm run opencomputer -- webhooks create pr-review-ingress \
--agent current \
--environment development
```
Each authenticated delivery starts a fresh durable session against the
active deployment and returns HTTP 202 with the session URL; a repeated
`Idempotency-Key` returns the original session instead of starting a second
review. See [Agent webhooks](/agents/webhooks).
GitHub webhooks sign with an HMAC header and cannot supply the bearer
token, so reviewing PRs on open requires a relay — for example a GitHub
Actions `pull_request` job holding the webhook URL and token as repository
secrets.
## Safety properties
* The GitHub token is write-only, attached outside the agent runtime, and
constrained to declared origin, method, and path prefix.
* The agent's toolset is exactly what the render selects; posting requires
an explicit request, and dry run is the default.
* Reviews are `COMMENT` events only; the agent cannot approve, request
changes, merge, or push.
* Diffs above 150,000 characters are truncated, and the tool result reports
the truncation and full size.
# A coding agent in Slack
Source: https://docs.opencomputer.dev/agents/examples/slack-coder
Deploy a coding teammate that takes work from Slack and opens draft pull requests
The Slack coder example is a small deployed agent that receives coding tasks
in Slack. Mention it with a task; it clones the repository on its OpenComputer
computer, changes code on a branch, runs the relevant checks, opens a draft
pull request and replies with the result. Mention it again in the same thread
to continue on the same branch and pull request. It keeps working after you
close your terminal.
The repository is two files:
```text theme={null}
opencomputer/project.ts
opencomputer/agents/coder/agent.ts
```
OpenComputer supplies the rest: the coding harness and computer, deployed
execution, conversation routing, GitHub credentials and the Slack app. There
is no server, message parser, GitHub client or Slack reply code to write.
[Browse the source on GitHub](https://github.com/diggerhq/opencomputer-slack-coder).
## What the agent does
`opencomputer/agents/coder/agent.ts` declares a GitHub connection with write
access to contents and pull requests, selects a model, enables the built-in
shell tool and returns the instructions the agent works from: read the
repository's instructions, inspect the code, work on a branch, run the checks
and report their actual results, push the branch and open a draft pull
request, continue follow-ups on that branch, never merge, and never print
credentials or put them in files or Git URLs.
The agent uses a dedicated Slack app: mentions and direct messages go to this
one agent.
## Deploy it
You need Node.js 22 or later, an OpenComputer account with available usage,
permission to install a Slack app in your workspace, and GitHub access to
install the managed OpenComputer App on the repositories the agent may change.
```bash theme={null}
git clone https://github.com/diggerhq/opencomputer-slack-coder.git
cd opencomputer-slack-coder
npm ci
npx --package @opencomputer/cli opencomputer login
npx --package @opencomputer/cli opencomputer link --create-project slack-coder
npm run deploy -- --alias production
```
The deploy command exits after publishing; nothing keeps running on your
machine. Open the printed project URL and select **Production**.
## Connect GitHub
In **Connections**, install the managed OpenComputer GitHub App, select the
repositories the agent may change, and attach the installation to Production.
That selection is the complete boundary of what the agent can reach. Anyone
who can message the bot can direct it at those repositories, so invite the bot
to conversations accordingly. See
[GitHub App connections](/agents/github).
## Connect Slack
In the same Connections tab, the Slack panel shows the agent that will receive
messages. Choose **Create Slack bot**, name it, paste an app configuration
access token from [your Slack apps](https://api.slack.com/apps), and approve
the installation in Slack. You are returned to the same project and
environment. Invite the bot to a channel and mention it once; the panel
reports the first message received.
The Connections panel also offers the manual setup alternative; see
[Slack connections](/agents/slack) for the full flow.
## Give it a task
Mention the bot by the name you gave it, in a channel it has been invited to,
naming a repository the installation can reach. The examples call it Patch:
> @Patch In acme/service, CSV export breaks when a customer's name contains a
> comma. Reproduce it, fix it, run the tests and open a draft PR.
The reply reports what was run, the check results and the draft pull request
link. Follow up in the same thread, from the same Slack account:
> @Patch Also cover names containing newlines in that PR.
The same session continues on the same branch and updates the same pull
request. A new thread starts independent work. Each conversation belongs to
the Slack user and thread it started in.
## Change the agent
Edit the instructions or the model in `agent.ts` and run the deploy command
again. New conversations use the promoted version; conversations already
running keep the version they started with. Redeploying does not require
reconnecting GitHub or Slack.
# Add high-value regression tests
Source: https://docs.opencomputer.dev/agents/examples/test-coverage
Review recent merges and open focused, test-only pull requests for material coverage gaps
The test coverage example reviews recently merged GitHub pull requests, finds
one meaningful regression risk with weak test coverage, adds the smallest
useful tests, and opens a focused pull request after those tests pass.
It does not optimize for a coverage percentage. Cosmetic changes, generated
files, broad snapshots, and behavior-neutral refactors are not reasons to
create a pull request. A run that finds no material gap finishes without
writing to GitHub.
## How it works
```mermaid theme={null}
flowchart LR
Merges[Recent merged pull requests]
Review[Risk and missing-test review]
Snapshot[Exact source snapshot and private baseline]
Tests[Minimal tests and relevant test commands]
Audit{Test-only audit passes?}
Pull[Focused pull request]
Report[No-op or blocker report]
Merges --> Review
Review -->|material gap| Snapshot
Review -->|no safe gap| Report
Snapshot --> Tests
Tests --> Audit
Audit -->|yes| Pull
Audit -->|no| Report
```
Every run examines a bounded recent-merge window and selects at most one
coherent gap. The agent downloads the exact default-branch commit it inspected,
keeps a private untouched baseline, follows the repository's own test
instructions, and runs the narrowest relevant validation commands.
The publishing tool compares the tested snapshot with the baseline. It rejects
production changes, unlisted paths, deletions, symlinks, and oversized files
before it can write to GitHub.
## Start with the example
> **[Deploy to OpenComputer](https://app.opencomputer.dev/new?repository-url=https%3A%2F%2Fgithub.com%2Fdiggerhq%2Fopencomputer-example-test-coverage)**
>
> Review the GitHub credential boundary and deploy the agent to Development.
[Browse the source on GitHub](https://github.com/diggerhq/opencomputer-example-test-coverage).
```bash theme={null}
git clone https://github.com/diggerhq/opencomputer-example-test-coverage.git
cd opencomputer-example-test-coverage
npm install
npm test
npm run typecheck
npm run opencomputer -- login
```
## Configure a fixture repository
Start with a disposable repository containing a recent merged change and a
deliberately missing regression test. Set that fixed destination in
`opencomputer/agents/test-coverage/tools/config.ts`:
```ts theme={null}
export const TARGET_REPOSITORY = {
owner: "your-github-owner",
repository: "coverage-agent-fixture",
defaultBranch: "main",
} as const;
export const PUBLISH_ENABLED = false;
```
The destination is code-owned. Pull-request text, prompts, commit messages, and
repository files cannot redirect the publishing tool to another repository.
Create a short-lived, fine-grained GitHub token restricted to the fixture
repository with **Contents: read and write** and **Pull requests: read and
write**. Pipe it into the stdin-only Development secret command:
```bash theme={null}
printf %s "$GITHUB_PAT" | npm run opencomputer -- secrets set GITHUB_PAT --value-stdin \
--environment development \
--agent current
```
The declared connection limits credential injection to GitHub API requests.
The token is not placed in agent source, prompts, repository URLs, or the
runtime environment. GitHub archive downloads follow the connection's declared
redirect to `codeload.github.com`; they do not require a second secret.
## Test in Development
Watch the source and deploy changes to the remote Development environment:
```bash theme={null}
npm run deploy -- --watch
```
In another terminal, start an explicit run:
```bash theme={null}
npm run session -- \
"Review recent merged code and add the highest-value missing regression tests."
```
With `PUBLISH_ENABLED` set to `false`, the agent can inspect the fixture, edit
its isolated snapshot, run tests, and perform the final audit, but the
publishing tool returns a dry-run result without creating a branch or pull
request.
Review the selected risk, proposed test paths, and observed command results.
If they are correct, set `PUBLISH_ENABLED` to `true`, let the watch deployment
finish, and start a new session with the same request.
The published branch is deterministically named
`test/coverage-`. Repeating the run for the same repository head
returns the existing open pull request rather than creating a duplicate.
After changing the example source, wait for the Development watch deployment
to finish and start a new session. Existing sessions remain pinned to the
deployment with which they started.
## What a dry run looks like
A useful no-op still reports what it reviewed. This run ranked three recent
merges, identified an entitlement gate as the highest-risk candidate, and
named two concrete missing regression cases before deciding whether it could
safely edit or publish tests.

The report is evidence for a human reviewer, not permission to publish. The
agent proceeds only after it materializes the exact repository snapshot, reads
the real test harness, edits test-only paths, and observes the validation
commands succeed.
## Troubleshoot remote runs
### GitHub archive redirect is blocked
Current example source authorizes GitHub's archive redirect from
`api.github.com` to `codeload.github.com`. If a session reports
`502 egress-redirect-blocked` while materializing the repository, update the
example, wait for a new Development deployment, and start a new session.
### Filesystem capabilities and launch trust boundary
The example explicitly registers the runtime-provided `bash` and `read` tools
in `opencode.json` and exposes them from the agent render. They let the agent
inspect, edit, and test the materialized repository snapshot. If an older
deployment reports `Unknown tool: bash`, `Tool is not available for this
request: read`, or `No Code Mode tools are available`, update the example, wait
for a new Development deployment, and start a new session.
These are broad capabilities. Repository files and test scripts are untrusted
code and may execute inside the session runtime, and file reads are not
enforced as repository-only. For the initial launch, use a disposable or
non-production repository, restrict the fine-grained token to that repository,
keep `PUBLISH_ENABLED` set to `false`, and review the dry-run result before
enabling publication.
The agent prompt limits shell and file-reading work to the exact materialized
snapshot and reserves GitHub operations for audited code-defined tools. Those
instructions are behavioral guardrails, not a sandbox boundary. Do not target a
sensitive or production repository until a constrained repository executor
replaces the general-purpose filesystem tools.
## Safety boundaries
* The agent can add or update tests and fixtures, but it cannot publish a
production-code testability refactor.
* It cannot merge, approve, close, or label pull requests or change repository
settings.
* Failing, flaky, or environment-dependent tests are reported without
publishing.
* Repository content and test output are treated as untrusted evidence rather
than instructions.
* The initial launch uses general-purpose filesystem tools inside the session
runtime; repository and token scoping remain required even with prompt
guardrails.
* This example starts from explicit interactive runs. Add a recurring schedule
only after validating the repository-specific workflow and publication
policy.
# GitHub App connections
Source: https://docs.opencomputer.dev/agents/github
Give an agent direct Git and GitHub API access with the managed OpenComputer App
A managed GitHub connection lets an agent clone repositories, push branches,
use the `gh` CLI, and call GitHub's REST or GraphQL APIs as a GitHub App
installation.
## Declare access
Declare the permissions the agent needs and reference the connection from its
agent function:
```tsx theme={null}
import {
defineConnection,
githubApp,
useConnection,
} from "@opencomputer/agent";
const github = defineConnection({
id: "github",
provider: githubApp({
permissions: {
contents: "write",
pull_requests: "write",
},
}),
});
export default function Agent() {
useConnection(github);
return "Work on the requested repository, push a branch, and open a PR.";
}
```
The supported permissions are `actions`, `checks`, `contents`, `issues`,
`metadata`, and `pull_requests`. `metadata` is read-only. Request only what the
agent needs.
GitHub access applies to the session, not to an individual render. OpenComputer
mints a token with the combined permissions of the deployment's declared
GitHub connections; if one declares read access and another write access for the
same permission, the token requests write access. Omitting or conditionally
calling `useConnection(github)` does not withhold or revoke that token.
Effective authority is therefore the intersection of:
* the managed OpenComputer GitHub App's permission ceiling;
* the combined permissions declared in `githubApp({ permissions })`; and
* the repositories selected in the attached GitHub installation.
## Install the App
Deploy the agent, then connect the managed App from the project directory:
```bash theme={null}
opencomputer github connect
```
Open the printed URL and install the App into a user or organization account.
The CLI waits until GitHub completes the flow and, by default, attaches the
installation to both development and production. Pass
`--environment development` or `--environment production` to attach only one
environment, or `--no-wait` when using the command in a non-interactive script.
Check the result at any time with:
```bash theme={null}
opencomputer github status
```
If the OpenComputer account already has one active GitHub App installation,
the command attaches it directly without opening GitHub again. When multiple
installations are available, select one with `--connection `; the
ids and account names are shown by `opencomputer github status --json`.
To install the App on another user or organization account, force a new GitHub
flow and select the target account in GitHub:
```bash theme={null}
opencomputer github connect --new
```
`--new` and `--connection` are mutually exclusive. Completing the new flow
attaches that installation to the requested project environment or, when no
environment is specified, to both environments.
You can also install or attach the App from the project's **Connections** page
in the OpenComputer dashboard. GitHub lets the installer select all
repositories or a specific set of repositories.
GitHub App access is intentionally separate from managed service OAuth. Do not
use `opencomputer connection add github`; the CLI directs that command to the
GitHub App flow instead.
That GitHub installation selection is the complete repository boundary. There
is no second OpenComputer repository allowlist. If the installer selects all
repositories, the agent can access every repository available to that
installation; if specific repositories are selected, it can access only those
repositories. Change the selection in GitHub's installation settings.
Connections are attached independently to the project's development and
production environments. A session uses the installation attached to the
environment it was created in: a session started as `worker@production` gets
the production installation's credentials even when the same deployment is
also the development alias. Promote one artifact to both environments with
different installations and each environment's sessions reach only their own
installation's repositories.
## List the repositories an environment can reach
An application that lets a person pick a repository reads the installation's
current selection from the connection rather than holding a GitHub credential
of its own:
```text theme={null}
GET /projects//github/repositories?environment=development&limit=100
```
`environment` is `development` or `production` and is required. `limit` is at
most 100, the default; `cursor` is the `nextCursor` of the previous page.
```json theme={null}
{
"repositories": [
{
"id": 1296269,
"fullName": "acme/service",
"private": true,
"defaultBranch": "main",
"archived": false
}
],
"nextCursor": null
}
```
The list is read live from GitHub each time, with a token minted for that read
that can see repository metadata and nothing else. OpenComputer keeps no copy:
change the selection in GitHub's installation settings and the next read shows
it. Call this route from your server with an organization API key.
| Status | Code | Meaning |
| - | - | - |
| `400` | `invalid_environment`, `invalid_limit`, `invalid_cursor` | The query is malformed |
| `404` | `github_connection_not_found` | The environment has no GitHub installation attached |
| `409` | `installation_suspended` | GitHub reports the installation suspended |
| `502` | `github_unavailable` | GitHub could not be read; retry |
## Use Git and the API
Inside the agent sandbox, ordinary GitHub tooling works directly. `gh` reads
the injected `GH_TOKEN` automatically:
```bash theme={null}
git clone https://github.com/acme/service.git
git -C service switch -c agent/update
git -C service push -u origin agent/update
gh pr create --repo acme/service --fill
gh api repos/acme/service/pulls
```
GitHub installation tokens expire after about one hour. Before dispatching a
command, OpenComputer attempts renewal if the held token would expire within
the command's allowed duration. Git and `gh` pick up a renewed token without
changes to the command. Renewal uses the same declared permissions and the
installation's current repository selection.
If renewal fails, the command still runs with the held token and its GitHub
requests may fail authentication. Detaching the connection prevents new
tokens from being issued; it does not immediately revoke a token already on
the computer. That token can remain usable until GitHub expires or revokes it.
### Understand GitHub permission responses
Installation tokens do not have OAuth scopes. That does not mean they are
unscoped. Their authority comes from the permissions included when the token is
minted and the repositories granted to the installation.
`X-Accepted-GitHub-Permissions` describes the permissions an API **endpoint
accepts or requires**. It does not describe the permissions granted to the
token. In particular, `allows_permissionless_access=true` means that endpoint
does not require a specific GitHub App permission; it does not mean the token
has unrestricted or “permissionless” repository access. Likewise, a
`permissions` object in an ordinary repository response is not an installation
token introspection result.
Use installation endpoints with an installation token. For example,
`GET /installation/repositories` lists the repositories available to the
installation. User endpoints such as `GET /user/repos` require a user token, so
a `403` from those endpoints is expected and says nothing about whether the
declared installation permissions were applied.
See GitHub's documentation for
[generating an installation access token](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation#generating-an-installation-access-token),
[troubleshooting required permissions](https://docs.github.com/en/rest/using-the-rest-api/troubleshooting-the-rest-api#resource-not-accessible),
and
[listing repositories accessible to an installation](https://docs.github.com/en/rest/apps/installations#list-repositories-accessible-to-the-app-installation).
> [!WARNING]
>
> The short-lived token is available inside the agent's sandbox as
> `GH_TOKEN` and `GITHUB_TOKEN`. Code running in that sandbox
> can read it. Do not print it, write it into source, add it to a Git remote,
> include it in a checkpoint, or send it to logs. Install the App only on
> repositories you are comfortable granting to the agent.
Other `defineConnection()` providers continue to use OpenComputer's managed
egress and secret proxy. This direct-token behavior is specific to managed
GitHub App connections.
# Agent hooks
Source: https://docs.opencomputer.dev/agents/hooks
Compose agent capabilities with reactive hooks
An agent function returns instructions. Hooks give that agent the model,
tools, services, and context it needs to follow those instructions.
```tsx theme={null}
import { useInput, useModel, useTool } from "@opencomputer/agent";
export default function Agent() {
const input = useInput();
useModel("anthropic/claude-sonnet-4.6");
if (input.text?.includes("latest")) {
useTool("web-search");
}
return "Answer directly. Use current sources when web search is available.";
}
```
## How hooks work
OpenComputer renders the exported function before each model step. Every hook
called during that render contributes to the next step's configuration. Hooks
describe capabilities; they do not call the model or run a tool themselves.
Unlike React component hooks, OpenComputer resource hooks can be conditional.
The example above exposes `web-search` only when the current input asks for
recent information.
Hooks can only run while OpenComputer is rendering an agent. Keep network
requests and other asynchronous work inside [tools](/agents/tools), not in the
agent function.
## Built-in hooks
| Hook | What it does | Guide |
| - | - | - |
| `useInput()` | Reads the current input source, text, and structured payload | [Inputs](/agents/inputs) |
| `useCurrentInput()` | Alias for `useInput()` | [Inputs](/agents/inputs) |
| `useModel(model)` | Selects the model used for the next step | [Models](/agents/models) |
| `useTool(tool)` | Exposes a named or code-defined tool | [Tools](/agents/tools) |
| `useMcpServer(server)` | Exposes tools from a managed HTTPS MCP server | [MCP servers](/agents/mcp) |
| `useSessionData(key)` | Reads a durable value associated with the session | [Session data](/agents/session-data) |
| `useMemory(memory)` | Reads bound memory and selects its permitted tools for this model request | [Memory](/agents/memory) |
[Skills](/agents/skills) are also agent capabilities, but they are discovered
from the agent's `skills/` directory rather than attached with a hook.
## Compose your own hooks
Hooks are ordinary synchronous functions. Group related capabilities in a
custom hook when several agents use the same setup:
```tsx theme={null}
import { useTool } from "@opencomputer/agent";
function useGitHubResearch() {
useTool("github-search");
return "Check GitHub before making claims about repositories or issues.";
}
export default function Agent() {
const githubInstructions = useGitHubResearch();
return `Research the request carefully. ${githubInstructions}`;
}
```
Custom hooks keep agent functions readable and make capability bundles
reusable. Prefix them with `use` so their purpose is obvious.
## Next steps
* [Read and route inputs](/agents/inputs)
* [Choose a model](/agents/models)
* [Give the agent tools](/agents/tools)
* [Connect an MCP server](/agents/mcp)
* [Add reusable skills](/agents/skills)
# Inputs
Source: https://docs.opencomputer.dev/agents/inputs
Read text, structured payloads, and input sources with useInput
`useInput()` reads the input that caused the current agent render. Use it to
include the current request in the instructions or change behavior based on
where the request came from.
```tsx theme={null}
import { useInput } from "@opencomputer/agent";
export default function Agent() {
const input = useInput();
return input.text
? `Help the user with this request: ${input.text}`
: "Help the user with their request.";
}
```
`useCurrentInput()` is an alias for `useInput()`.
## Input shape
```ts theme={null}
interface AgentInput {
readonly source: InputSource;
readonly text?: string;
readonly payload?: DataValue;
readonly answer?: QuestionAnswer;
readonly steering?: readonly HeldInput[];
}
```
| Field | Meaning |
| - | - |
| `source` | How the work entered the session: `user`, `channel`, `schedule`, `webhook`, `subagent`, `system` or `event` |
| `text` | Optional human-readable message |
| `payload` | Optional structured JSON value supplied by the source |
| `answer` | Present only when this input answers the question an earlier turn asked; see [Answers and steering](#answers-and-steering) |
| `steering` | Inputs written while that question was open that did not answer it, in order |
`DataValue` means any JSON-compatible value: `null`, a boolean, number,
string, array, or object. Both `text` and `payload` are optional, so check them
before use.
A payload comes from whatever started the turn. An application sends one on
the [turns route](/agents/api#turns) next to the text, and the agent reads it
with `source: "user"`; a [webhook](/agents/webhooks) delivers its body as the
payload; a [schedule](/agents/schedules) dispatches the payload it was
configured with. The agent reads all of them the same way.
A message from a connected [Slack app](/agents/slack) arrives with
`source: "channel"` and a `channel` object naming the `provider` (`slack`),
the `connectionId`, and the Slack `workspaceId`, `conversationId` and `userId`
when Slack supplies them. An agent serving one workspace can ignore all of
this; an agent behind a shared Slack app uses it to decide whose records to
read.
A message from a [Linear connection](/agents/linear) also arrives with
`source: "channel"`, with `channel.provider` set to `linear`,
`channel.workspaceId` the Linear workspace, `channel.conversationId` the
Linear agent session and `channel.userId` the person who wrote the message
(on the first message, the session's creator; absent when automation started
it).
Its `payload` is a `LinearInputPayload`: the issue, the session, the thread's
comments, your workspace's guidance for agents, and `origin`, which says
whether the issue had a parent and whether this agent's own app created it.
`origin` is read when the session starts and does not change afterwards. The
fields are listed on [Linear connections](/agents/linear#what-it-receives).
```tsx theme={null}
import { useInput, type LinearInputPayload } from "@opencomputer/agent";
export default function Agent() {
const input = useInput();
if (input.source === "channel" && input.channel.provider === "linear") {
const linear = input.payload as unknown as LinearInputPayload;
return linear.origin.createdByAgent && linear.origin.parent
? `Implement ${linear.issue.identifier}, part of ${linear.origin.parent.identifier}.`
: `Investigate ${linear.issue.identifier} and propose an approach.`;
}
return `Help with: ${input.text ?? "the current request"}`;
}
```
## Adapt to the source
The current direct and delegated flows can share one agent definition:
```tsx theme={null}
import { useInput } from "@opencomputer/agent";
export default function Agent() {
const input = useInput();
return input.source === "subagent"
? "Complete the delegated task and return a concise result."
: `Help with: ${input.text ?? "the current request"}`;
}
```
## Read structured payloads
Narrow or validate a payload before reading application-specific fields:
```tsx theme={null}
import { useInput } from "@opencomputer/agent";
type ResearchRequest = {
topic?: string;
depth?: "brief" | "deep";
};
export default function Agent() {
const input = useInput();
const request = (input.payload ?? {}) as ResearchRequest;
if (request.topic) {
return `Research ${request.topic} at ${request.depth ?? "brief"} depth.`;
}
return "Ask for a research topic before starting.";
}
```
Validate untrusted payloads in a tool before performing side effects.
## Event input
When an [event subscription](/agents/api#event-subscriptions) delivers
another session's turn outcome to this one, the turn's input has
`source: "event"` and an `event`:
```tsx theme={null}
import { useInput } from "@opencomputer/agent";
export default function Agent() {
const input = useInput();
if (input.source === "event") {
const { event } = input;
if (event.type === "turn.completed") {
return `Worker ${event.agentId} finished turn ${event.turnId}. Its final message: ${event.result?.text ?? "(none)"}. Decide what happens next.`;
}
return `Worker ${event.agentId}'s turn ${event.type === "turn.failed" ? "failed" : "was cancelled"} (${event.reason ?? "no reason"}). ${event.error ?? ""}`;
}
return `Help with: ${input.text ?? "the current request"}`;
}
```
| Field | Meaning |
| - | - |
| `event.id` | The source session's event id for its terminal `turn.*` event |
| `event.type` | `turn.completed`, `turn.failed` or `turn.cancelled` |
| `event.sessionId`, `event.turnId`, `event.agentId` | Whose outcome this is |
| `event.occurredAt` | When the source turn settled |
| `event.reason` | Why it failed or was cancelled, when recorded |
| `event.error` | The failure message, bounded, when it failed |
| `event.result` | `{ text, truncated }`: the final assistant message of a completed turn, bounded to 16 KB |
An event input carries no `text`; read `event`. The turn's recorded input in
the [event log](/agents/events) is a text description of the same outcome.
The platform attests where the input came from through `source`; the
included message is another agent's output and is data to reason about, not
instructions to follow.
## Answers and steering
A turn can end with a question: the agent selects the
[`ask`](/agents/tools#ask-a-question) tool and the model calls it. The
reply is the next input, from any source, with `answer` set:
| Field | Meaning |
| - | - |
| `answer.questionId` | The question this input answers |
| `answer.text` | What the person wrote or selected |
| `answer.value` | The chosen option's `value`, when the text matched an option by value or label |
Input that arrives while the question is open without answering it does not
run a turn of its own. It is held and delivered with the answer as
`steering`, in the order it arrived; past a bounded number of items or 64 KiB,
the rest run as ordinary turns after the answer. In Linear, `steering` is
usually empty; see [Ask before acting](/agents/linear#ask-before-acting).
| Field | Meaning |
| - | - |
| `text`, `payload`, `channel` | The held input, as it would have arrived on its own |
| `receivedAt` | When OpenComputer received it |
| `providerTime` | When the provider says it was written, when it says |
Read `steering` before acting on the answer. A constraint written while the
question was open can change what a "go" means, and the agent should revise
its proposal rather than act on an answer the steering has overtaken.
```tsx theme={null}
import { useInput, useTool } from "@opencomputer/agent";
export default function Agent() {
const input = useInput();
if (input.answer) {
const held = (input.steering ?? []).map((item) => `- ${item.text ?? ""}`).join("\n");
return `The person answered: ${input.answer.value ?? input.answer.text}.${
held ? `\nWritten meanwhile, read before acting:\n${held}` : ""
}`;
}
useTool("ask");
return "Read the request, propose a plan, and ask before making changes.";
}
```
`answer` and `steering` sit beside `payload`, not inside it, so they read the
same from every source and never collide with an application's payload.
## Input is not a prompt UI
`useInput()` only reads admitted input. It does not itself pause the session
to ask a person a question; a turn asks with the [`ask`](/agents/tools#ask-a-question)
tool, and the answer arrives as a later input. The durable conversation and
later messages are managed by the [session](/agents/sessions).
Input payload also differs from [session data](/agents/session-data): a payload
belongs to the current input, while session data can persist across renders.
# Linear connections
Source: https://docs.opencomputer.dev/agents/linear
Make a deployed agent a teammate people delegate issues to and mention in Linear
Connect an agent to Linear and people can delegate issues to it and mention it.
It appears under your Linear app's name and icon. Its tool calls and replies
appear in the issue's thread. You write no webhook or Linear client code.
```tsx theme={null}
import { useInput } from "@opencomputer/agent";
export default function Agent() {
const input = useInput();
return `You are a teammate in Linear. Work on this and reply in the thread:\n\n${input.text ?? ""}`;
}
```
## Connect
1. In the project's **Connections** tab, choose **Create Linear agent** on the
environment's row and name it. Names can't contain "Linear".
2. Choose **Open Linear to create the app**. The page is prefilled; create the app.
3. Paste the app's client ID, client secret and webhook signing secret.
4. Choose **Authorize in Linear**. A workspace admin must approve.
5. Delegate an issue to the agent. The panel shows **First session received**.
Each environment needs its own Linear app. Linear access tokens never reach the
agent, its logs or the browser.
## Talking to it
* **Delegate an issue** or **mention it** in a comment to start a session.
* **Reply in the session's thread** to continue, even after it finished.
A top-level comment without a mention is not delivered. Anyone on the team can
reply; the agent answers the thread, not a person.
## What it receives
Each message is a [channel input](/agents/inputs) with
`channel.provider: "linear"`. `text` is the message; for the first one it is
Linear's prompt context, or `: ` without one. `payload` is a
`LinearInputPayload`:
| Field | Meaning |
| - | - |
| `issue` | `{ id, identifier, title, description, url, team }` |
| `origin` | `{ parent?, createdByAgent }`, read when the session starts and fixed for its life; `createdByAgent` is true when this agent's Linear app created the issue, whoever delegated it |
| `action` | `created` for the first message, `prompted` after |
| `session` | `{ id, url?, creator? }`; `creator` is the person responsible, absent when automation started it |
| `promptContext`, `comment`, `previousComments`, `guidance` | The thread and your workspace's agent guidance, as Linear sent them |
## What it writes
| The agent | Linear shows |
| - | - |
| Receives a session | "Reading the issue.", at once |
| Calls a tool | An action with a short subject such as a file path, never full arguments or output |
| Finishes its turn | The final message as a response, or "Done." without one |
| Calls `ask` | A question awaiting input, options as buttons |
| Is stopped | An error: "Stopped." |
| Fails | An error: "OpenComputer could not complete this request." |
A GitHub pull request URL in the final message becomes the session's link.
## Ask before acting
```tsx theme={null}
import { useInput, useService, useTool } from "@opencomputer/agent";
import { applyPlan } from "./tools/apply-plan"; // a defineTool that calls Linear
export default function Agent() {
useService("linear");
const input = useInput();
if (!input.answer) {
useTool("ask");
return `Propose a plan, then ask whether to go:\n\n${input.text ?? ""}`;
}
useTool(applyPlan);
return `You were told "${input.answer.text}". If that means go, do it.`;
}
```
Any reply after the question is the answer, `input.answer`. Linear holds
messages typed while the agent works and releases them only when it finishes,
so they arrive after the answer as ordinary turns and `input.steering` is
usually empty; a stop discards them. Say so in the question, so people send
constraints before answering.
## Stop
Linear's stop button interrupts the turn, discards queued messages and closes
any open question. A later reply continues the same session.
## Call Linear
Declare the service in the agent and call Linear's GraphQL API from a tool:
```tsx theme={null}
// in the agent component
useService("linear");
// in a tool
const response = await callService({
service: "linear",
method: "POST",
path: "/graphql",
body: JSON.stringify({ query, variables }),
});
```
In sessions started from Linear, the call acts as the agent's own Linear app.
In a session a person started (the playground, your app), it acts as that
person's [connected Linear account](/agents/services). Only `POST /graphql` is
allowed. Issues the agent delegates to itself start new
sessions with `origin.createdByAgent: true`. For code, connect the project's
[GitHub App](/agents/github).
## Disconnect
Choose **Disconnect** in the Linear panel. The app stays in your Linear
workspace; delete it there if you no longer need it.
## Limits
* One connection per agent per environment, and one per Linear app.
* No concurrency limit: every delegated issue runs its own session.
* `ask` isn't available in sessions with `executionMode: "microvm"`.
* Stopping a session doesn't stop issues it delegated.
# Logs
Source: https://docs.opencomputer.dev/agents/logs
Inspect agent output and outbound-request activity from the OpenComputer CLI
OpenComputer collects agent output together with outbound-request events. Logs
are indexed by project, environment, agent, and session so you can
inspect failures from the project or CLI.
## Read the current agent's logs
From an initialized project:
```bash theme={null}
npx --package @opencomputer/cli opencomputer logs
```
The CLI uses the local project binding and current agent automatically. Follow
new entries while developing:
```bash theme={null}
npx --package @opencomputer/cli opencomputer logs --follow
```
## Filter logs
```bash theme={null}
npx --package @opencomputer/cli opencomputer logs --agent hello-world --environment development
npx --package @opencomputer/cli opencomputer logs --session
npx --package @opencomputer/cli opencomputer logs --limit 200
```
Use JSON Lines for scripts and log processors:
```bash theme={null}
npx --package @opencomputer/cli opencomputer logs --follow --json
```
Each entry includes a stable cursor, timestamp, level, source, message, and
the applicable agent or session identifiers.
## Secret safety
Outbound-request logs describe the connection, method, destination path, status,
and timing. They do not contain injected secret values or secret-bearing
headers. Request headers that could override managed credentials are discarded
before the outbound request is sent.
# MCP servers
Source: https://docs.opencomputer.dev/agents/mcp
Attach managed HTTPS MCP servers with useMcpServer
MCP (Model Context Protocol) connects an agent to tools hosted by an external
service. Declare the server once with `defineMcpServer()`, then attach it to an
agent render with `useMcpServer()`.
## Connect a server
```tsx theme={null}
import {
defineMcpServer,
useMcpServer,
useModel,
} from "@opencomputer/agent";
const docs = defineMcpServer({
id: "company-docs",
url: "https://mcp.example.com/server",
});
export default function Agent() {
useModel("anthropic/claude-sonnet-4.6");
useMcpServer(docs);
return "Answer questions using the company documentation when relevant.";
}
```
Managed MCP URLs must use HTTPS. OpenComputer connects to the server and makes
its tools available to the managed agent loop; agent code does not open or
close the connection itself.
## Definition fields
| Field | Required | Purpose |
| - | - | - |
| `id` | yes | Stable ID for the server in the deployment |
| `url` | yes | HTTPS MCP endpoint |
| `connection` | no | Secret-backed HTTP connection used to authorize the server |
`useMcpServer()` accepts either a definition or an existing server ID.
## Attach authorization
Reference a secret-backed HTTP connection when the server requires
authorization:
```tsx theme={null}
import {
bearer,
defineConnection,
defineMcpServer,
useSecret,
useMcpServer,
} from "@opencomputer/agent";
const github = defineConnection({
id: "github-mcp-auth",
origin: "https://api.githubcopilot.com",
headers: {
Authorization: bearer(useSecret("GITHUB_PAT")),
},
});
const githubMcp = defineMcpServer({
id: "github",
url: "https://api.githubcopilot.com/mcp/",
connection: github,
});
export default function Agent() {
useMcpServer(githubMcp);
return "Use GitHub tools to investigate repository questions.";
}
```
The declaration contains only a secret reference. Set the secret with
`opencomputer secrets set GITHUB_PAT`; its value is injected only into requests
to the declared origin. See [Secrets and outbound requests](/agents/secrets).
## Attach MCP conditionally
An agent can expose a server only when the current request needs it:
```tsx theme={null}
import { useInput, useMcpServer } from "@opencomputer/agent";
export default function Agent() {
const input = useInput();
if (input.text?.toLowerCase().includes("documentation")) {
useMcpServer(docs);
}
return "Use company docs when that capability is available.";
}
```
Conditional attachment reduces the number of tools the model must choose from
on unrelated requests.
## MCP versus code-defined tools
Use [code-defined tools](/agents/tools) when you own the operation or need
custom validation and control. Use MCP when a service already publishes the
tools you need or when several agents should consume the same remote tool set.
# Memory
Source: https://docs.opencomputer.dev/agents/memory
Read and save notes across sessions
Memory stores requirements, decisions and preferences that later sessions can
reuse. Owners can read, edit and export the same notes.
## Declare a resource
A resource is declared once, at module scope, like a tool or a connection:
```ts theme={null}
import { defineMemory } from "@opencomputer/agent";
export const notes = defineMemory({
id: "notes",
description: "Confirmed workshop requirements.",
});
```
`defineMemory` uses the built-in `documentMemory` provider by default:
OpenComputer stores editable text, without starting a sandbox. Deploying the
resource registers it; [create a document](/agents/document-memory#owner-access)
before binding it to a session.
`notes` is an application-defined resource name. It can contain many documents;
this example calls one `workshop`. Neither name is built into OpenComputer.
Declare with a direct `defineMemory({...})` on a literal object, calling the
function imported by name from `@opencomputer/agent` (or re-exported under
that name by one of your own modules); the compiler rejects aliases,
namespace members such as `agent.defineMemory`, spreads and variables so
that what it registers is exactly what runs. Anything else that merely
spells `defineMemory`, such as a property of your own, is unrelated to it.
## Read it in the render
`useMemory` returns the bound text and selects the memory tools for the next
model step:
```ts theme={null}
import { useMemory } from "@opencomputer/agent";
import { notes } from "./memory";
export default function Agent() {
const memory = useMemory(notes);
return `Help plan the workshop.
Save changes with memory_save and keep the other confirmed requirements.
## Confirmed requirements
${memory.text}`;
}
```
The hook is synchronous and returns `{ text, sources, writable }`. Your agent
decides where the text goes in its instructions; nothing is inserted for you.
Treat saved text as data the model reads, not as instructions it follows.
## Bind a document to a session
Include `memory` when creating a [session](/agents/sessions) with
`POST /api/managed-agents/sessions` ([management API](/agents/api#create)):
```json theme={null}
{
"agentId": "@development",
"memory": {
"notes": {
"scope": "document",
"id": "workshop",
"access": "read-write"
}
}
}
```
| Setting | Effect |
| - | - |
| `notes` | Matches the declared resource ID. |
| `id: "workshop"` | Selects one existing document from that resource. |
| `access: "read-write"` | Supplies its text and the `memory_save` tool. Use `read` for text only. |
Bindings are fixed for the session. The model receives neither storage
credentials nor permission to select another document. A
[collection binding](/agents/document-memory#session-bindings) instead lets it
browse documents with `memory_list` and `memory_read`.
The document must exist before the session binds it. `sessions.startOnDocument`
on the [TypeScript client](/reference/typescript-sdk/agents)
creates it if needed and then the session, and the CLI's
`session create --memory notes=workshop --create-document` does the same.
## Save and reuse notes
Saving is explicit: the model calls `memory_save({ text })`. Each save replaces
the document's text, so the example asks it to preserve existing requirements.
Conversation history is not automatically extracted into memory.
Sessions bound to the same document share its latest saved text. Each model
step reads it again; [revision checks](/agents/document-memory#saving) prevent
an agent from overwriting changes made since its read. A successful save is
reported as a `memory.saved` [session event](/agents/events#memory), delivered
best-effort.
Memory belongs to a **project and environment**. It survives session end,
sandbox loss, conversation compaction and new deployments. Development and
Production have separate stores.
## Reference
* [Document memory](/agents/document-memory): bindings, tools, conflicts,
owner operations and API contracts.
* [Memory providers](/agents/memory-providers): storage responsibilities and
the proposed external-provider contract.
* [Session data](/agents/session-data): application values scoped to one session.
# Memory providers
Source: https://docs.opencomputer.dev/agents/memory-providers
Provider responsibilities, document memory and the proposed HTTP contract
Providers own storage, retrieval and writes. `defineMemory` declares a resource;
`useMemory` returns its `text`, `sources` and `writable`. Your render chooses
how to include the recalled text in the instructions.
> [!WARNING]
>
> HTTP memory is a contract preview: `httpMemory` declarations compile, but
> namespace bindings and HTTP execution are unavailable. Use
> [document memory](/agents/document-memory).
## Choosing a provider
| Concern | `documentMemory` | HTTP (preview) |
| - | - | - |
| Storage | Editable documents in OpenComputer. | Your service and index. |
| Recall | Full document or collection overview. | Context your service selects, such as relevant facts. |
| Tools | Writable document: `memory_save`. Collection: `memory_list`, `memory_read`. | Provider-defined, such as `memory_search` or `memory_remember`. |
| Writes | Replace text conditionally on the originating request's revision. | Provider-defined concurrency and visibility. |
| Owner controls | APIs for inspection, editing, export and deletion. | Your service's controls; no document API required. |
Hooks stay stable; bindings, tools, prompts and editors may change. Changing
provider kind requires a new resource and migration. A new endpoint copies no
data: confirm it addresses the existing store or migrate, including sessions
pinned to old deployments.
## Declare an HTTP provider
The deployment stores configuration; your endpoint executes provider code.
```ts theme={null}
import {
bearer, defineConnection, defineMemory,
httpMemory, useSecret,
} from "@opencomputer/agent";
const connection = defineConnection({
id: "memory-service",
origin: "https://memory.example.com",
methods: ["POST"],
pathPrefix: "/oc-memory",
headers: {
Authorization: bearer(useSecret("MEMORY_TOKEN")),
},
});
export const knowledge = defineMemory({
id: "knowledge",
description: "Verified facts needed in later sessions.",
provider: httpMemory({
connection,
path: "/oc-memory",
maxBytes: 8_192,
tools: [{
name: "remember",
description: "Save a verified fact.",
access: "write",
idempotent: false,
input: {
type: "object",
properties: {
fact: { type: "string", maxLength: 2_000 },
},
required: ["fact"],
additionalProperties: false,
},
}],
}),
});
```
| Setting | Requirement / default |
| - | - |
| `connection` | Required `defineConnection` permitting `POST`. [Managed secrets](/agents/secrets) supply credentials; the model never receives them. |
| `path` | Default `/memory`. Single leading `/`; no whitespace, query or fragment. Must satisfy connection policy. No redirects. |
| `maxBytes` | Recalled UTF-8 text: 1–16,384 bytes; default 8,192. |
| `tools` | Up to eight; default none. Each needs a unique name, description, access and input schema. |
| Tool `name` | 1–32 lowercase letters, digits or underscores. |
| Tool `access` | `read` or `write`. |
| Tool `idempotent` | Default `false`; see [retries](#deadlines-and-retries). |
| Tool `input` | Object JSON Schema 2020-12, without `$ref`, as for [code tools](/agents/tools). Top-level `memory` is reserved: omit it from properties and required fields. |
## Namespace and tool selection
Session `memory` binding (`knowledge` and `owner` are application-defined):
```json theme={null}
{
"knowledge": {
"scope": "namespace",
"id": "owner",
"access": "read-write"
}
}
```
| Binding rule | Contract |
| - | - |
| Namespace | Trusted application code selects it under project authority. ID: 1–128 letters, digits, hyphens or underscores. Access defaults to `read`. |
| Partition key | OpenComputer derives it from project, environment, resource and namespace. |
| Key stability | Stable across sessions and deployments. Different environments or namespaces have distinct keys. |
| Isolation | Scope every operation to the partition key. It identifies data; it is not a credential. |
| Recall | All bound resources are read before render, including resources whose hooks are omitted. Failure blocks inference. |
| Tools | `useMemory` selects permitted tools for that request. Read tools allow either access level; write tools require read-write. Omitting the hook hides its tools. |
| `writable` | Reflects binding access, not guaranteed write success. |
Fixed tool names use `memory_`, here `memory_remember`.
* **Target:** optional `memory` selects an eligible resource from the originating
render; required when more than one qualifies. OpenComputer strips it, then
validates provider arguments, including `additionalProperties: false`.
The endpoint receives the partition instead.
* **Rejection:** a wrong target returns `rejected` / `not_bound` with eligible
targets. Changed access rejects the call; it never redirects it.
* **Collisions:** identical names merge only with identical schemas, retaining
each target's descriptions, access checks and retry policy. Incompatible
schemas or collisions with ordinary tool names reject session creation.
## Endpoint protocol
All operations use `POST`, `Content-Type: application/json`, `version: 1`.
Authenticate the connection credential, enforce read-only access, reject
expired deadlines and prevent arguments selecting another partition.
### Recall
Request:
```json theme={null}
{
"version": 1,
"operation": "recall",
"operationId": "mrq_123:knowledge",
"partition": "ocmem_v1_opaque-partition",
"access": "read-write",
"deadlineAt": "2026-09-10T15:04:15.000Z",
"input": { "text": "What hosting did we choose?" },
"maxBytes": 8192
}
```
Response, HTTP 200:
```json theme={null}
{
"text": "No paid external services for the demo.",
"sources": [{
"id": "fact:hosting",
"title": "Hosting constraint"
}],
"cursor": "provider-snapshot-42"
}
```
| Field | Contract |
| - | - |
| `input.text` | Current input text, or `null`. No conversation history, structured payloads, other resources' memory, OpenComputer grants or runtime credentials. Send only to trusted services. |
| `text`, `sources` | Required. Empty memory: `""`, `[]`. |
| Source | Required string `id`; optional strings `title`, `revision`, `updatedAt` (ISO 8601). IDs are provider-local references, not grants. |
| `cursor` | Optional read state for optimistic concurrency. The host retains it for this request's tools; it is absent from the public projection and cannot be chosen by the model. |
Recall has no storage side effects. OpenComputer persists the accepted
projection before inference and reuses it on request retries. Before
acceptance, recall retries may see newer data. New model requests recall again.
No automatic turn-completion or compaction callbacks. Conversation-fed
services need a separate ingestion path.
### Tools
Endpoints cannot add tools or widen access beyond deployment declarations.
Requests contain validated arguments and the recall cursor, if supplied:
```json theme={null}
{
"version": 1,
"operation": "tool",
"operationId": "toolcall_456:knowledge",
"partition": "ocmem_v1_opaque-partition",
"access": "read-write",
"deadlineAt": "2026-09-10T15:04:25.000Z",
"tool": "remember",
"arguments": {
"fact": "Use the free hosting tier for the demo."
},
"cursor": "provider-snapshot-42"
}
```
Return HTTP 200; any JSON `result` reaches the model unchanged:
```json theme={null}
{ "result": { "status": "stored" } }
```
| Write status | Meaning |
| - | - |
| `stored` | Available to recall. |
| `accepted` | Queued upstream; recall may lag. |
Tool descriptions must explain visibility. Providers own write semantics;
document-memory revision checks do not apply.
For conflicts, return HTTP 409 with this error envelope (`details` optional):
```json theme={null}
{
"error": {
"code": "conflict",
"message": "Knowledge changed; reconcile it.",
"details": {}
}
}
```
All errors use this envelope and reach the model without advancing the
cursor. Tools from one response share it; the next request recalls again.
## Deadlines and retries
| Rule | Contract |
| - | - |
| Deadline | Ten seconds across all attempts. |
| Retryable failures | Transport failures, HTTP 408, 429 and 5xx. No other 4xx retries. |
| Attempts | At most two retries, after 250 ms and one second. Valid `Retry-After` takes precedence if it fits the deadline. Same operation ID and body. |
| Eligible operations | Recall and read tools (both side-effect-free); writes only with `idempotent: true`. |
| Operation identity | Recall: model request plus resource. Tool: tool call plus resource. |
`idempotent: true` requires deduplication by `(partition, operationId)`,
rejection of changed tools/arguments/cursors, and replay of the original result
until `deadlineAt`. An adapter cannot guarantee this over an upstream service
that may commit a write before losing its reply.
With `idempotent: false`, transport failures and timeouts return
`{ "status": "uncertain" }` without retry. The next request recalls current
state; timeout does not prove failure.
| Limit | Value |
| - | - |
| Request or response body | 64 KiB each, including source metadata |
| Sources per response | 100 |
| Each source ID or cursor | 1,024 UTF-8 bytes |
| Recalled text | The declaration's `maxBytes` |
Invalid JSON, oversized output, authentication failures and exhausted retries
produce visible errors. Failed recall blocks inference, never substituting
empty memory. Tool errors imply no rollback. Anything placed in instructions
consumes model context.
## Lifecycle and owner controls
* **Access:** OpenComputer checks bindings before each call. Session end blocks
new calls after revocation; earlier calls may still commit remotely.
* **Cancellation:** aborts the request, without undoing accepted writes.
Stronger fencing belongs in the provider's storage protocol.
* **Ownership:** the provider supplies retention, backups, inspection, export
and deletion. OpenComputer document editing/freezing applies only to
`documentMemory`; deleting a project does not delete remote data.
* **Inspection:** the [resource inventory](/agents/document-memory#resource-inventory)
identifies each resource's provider. Session inspection would expose
namespace and partition so owners can locate remote data.
# How it works
Source: https://docs.opencomputer.dev/agents/mental-model
A mental model for agent functions, the managed harness, durable sessions, capabilities, and triggers
OpenComputer runs agents defined as TypeScript functions. You describe the
agent for the current step; OpenComputer supplies the managed harness that
calls the model, executes tools, streams output, and records the durable
session.
```mermaid theme={null}
flowchart LR
Trigger[Message, schedule, webhook, or event]
Session[Durable session]
Harness[Managed harness]
Function[Your agent function]
Model[Model]
Tools[Tools and MCP servers]
Trigger --> Session
Session --> Harness
Harness --> Function
Function -->|instructions, model, capabilities| Harness
Harness --> Model
Model -->|tool request| Harness
Harness --> Tools
Tools -->|result| Harness
Harness -->|events and messages| Session
```
## The runtime model
### Your agent function describes the next step
The default export is a synchronous function. It reads the current input,
selects resources with hooks, and returns instructions:
```tsx theme={null}
import { useInput, useModel, useTool } from "@opencomputer/agent";
import { inspectRepository } from "./tools/inspect-repository";
export default function Agent() {
const input = useInput();
useModel("anthropic/claude-sonnet-4.6");
useTool(inspectRepository);
return `
You are a release-readiness agent.
Inspect the repository before giving a RELEASE or DO NOT RELEASE verdict.
Current request: ${input.text ?? "Review the configured repository."}
`;
}
```
The function does not call the model or execute the tool. Each hook contributes
to the effective configuration for the next model step.
### The harness performs the loop
OpenComputer owns the agent harness. For each step it:
1. renders the agent function;
2. builds a model request from the returned instructions and selected resources;
3. streams the model response;
4. validates and executes requested tools;
5. records the resulting events; and
6. renders again when another model step is required.
```mermaid theme={null}
sequenceDiagram
participant S as Session
participant H as Managed harness
participant A as Agent function
participant M as Model
participant T as Tool
S->>H: Start or resume a turn
H->>A: Render current input and state
A-->>H: Instructions and capabilities
H->>M: Model request
M-->>H: Tool request
H->>T: Execute
T-->>H: Result
H->>S: Record events
H->>A: Render the next step
A-->>H: Updated configuration
H->>M: Continue
M-->>H: Final response
H->>S: Record completion
```
Because a function can render more than once during a turn, keep it synchronous
and free of external side effects. Network calls and mutations belong in
[tools](/agents/tools) or remote [MCP servers](/agents/mcp).
### A session makes execution durable
A [session](/agents/sessions) stores the conversation and runtime events across
turns. Clients can disconnect, reconnect, and continue without reconstructing
the agent's history themselves.
Every session remains pinned to the immutable deployment it started with. A
new development sync or production deployment affects new sessions, not code
already running inside an existing session.
[Memory](/agents/memory) is what outlives a session: project-scoped documents
that the agent function reads at render with `useMemory`, that the model saves
through memory tools, and that the owner reads, edits and exports.
## Capabilities are selected, not executed, during render
Use the narrowest capability that fits the work:
| Capability | What the harness receives |
| - | - |
| `useTool(tool)` | One typed operation implemented by your project |
| `useMcpServer(server)` | A collection of tools advertised by a remote MCP server |
| A skill under `skills/` | Reusable instructions and supporting resources |
| `useMemory(memory)` | The bound documents' text and the tools that save to them |
The model decides whether to request an available tool. The harness remains
responsible for validating the request, executing it, and recording the
result. See [Agent capabilities](/agents/capabilities) for the full comparison.
## Deployment registers; rendering selects
A deployment packages the agents and resources that are allowed to exist. A
render selects the tools, instructions and other resources for one model step.
[Managed GitHub access](/agents/github#declare-access) is session-wide: its
token carries the deployment's declared GitHub permissions. Conditional
`useConnection()` calls do not change that token's authority.
```mermaid theme={null}
flowchart LR
Source[Project source]
Deployment[Immutable deployment]
Session[Session pinned to deployment]
Render[Agent render]
Step[Instructions, model, selected capabilities]
Source --> Deployment
Deployment --> Session
Session --> Render
Render --> Step
```
Development and Production point to different deployments and keep operational
configuration—such as secrets, schedule state, and webhook tokens—separate.
Read
[Deployments and environments](/agents/deployments) for that lifecycle.
## Remember this
> **The agent function decides what the agent is for this step.**\
> **The harness decides how to run that step.**\
> **The session remembers what happened.**
## Explore each concept
> **[Reactive agents](/agents/reactive-agents)**
>
> Define instructions and select resources with ordinary TypeScript.
> **[Sessions and turns](/agents/sessions)**
>
> Understand durable conversations and runtime events.
> **[Capabilities](/agents/capabilities)**
>
> Choose between tools, MCP servers, and skills.
> **[Schedules](/agents/schedules)**
>
> Start recurring sessions from code-defined cron schedules.
> **[Webhooks](/agents/webhooks)**
>
> Start sessions from authenticated external services.
> **[Database](/agents/database)**
>
> Store application state and durable work for application-owned workers.
# Models
Source: https://docs.opencomputer.dev/agents/models
Select a model with useModel
`useModel()` selects the model that powers the next agent step. OpenComputer
handles provider authentication and response streaming.
```tsx theme={null}
import { useModel } from "@opencomputer/agent";
export default function Agent() {
useModel("anthropic/claude-sonnet-4.6");
return "You are a helpful assistant. Be concise and practical.";
}
```
`useModel()` is a declaration. It returns nothing and does not make a model
request itself.
## Model identifiers
Use a `provider/model` string:
```tsx theme={null}
useModel("anthropic/claude-sonnet-4.6");
```
The object form is equivalent:
```tsx theme={null}
useModel({
provider: "anthropic",
model: "claude-sonnet-4.6",
});
```
Keep model selection in agent code so development and immutable deployments
record the behavior together. Provider credentials are not added to source
bundles or client applications.
## Select a model conditionally
Because the agent re-renders for each model step, selection can respond to the
current input:
```tsx theme={null}
import { useInput, useModel } from "@opencomputer/agent";
export default function Agent() {
const input = useInput();
const needsDeepReview = input.text?.includes("deep review") ?? false;
useModel(
needsDeepReview
? "anthropic/claude-sonnet-4.6"
: "anthropic/claude-haiku-4.5",
);
return needsDeepReview
? "Review the request carefully and explain important tradeoffs."
: "Answer directly and briefly.";
}
```
Select one model for the next step and keep the choice predictable: model
changes affect cost, latency, context limits, and output behavior.
## Keep credentials out of agent code
Do not place provider API keys in `agent.ts`, tools, prompts, or the React
application. OpenComputer authenticates the selected provider without adding
the credential to agent code.
For credentials used by your own HTTP tools instead of model inference, use
[Secrets and outbound requests](/agents/secrets).
## Use a connected model account
Organizations on Pro or Max can connect a Codex account and route eligible
OpenAI models through it. Use the native `openai` provider when you want the
connected account; Managed OpenRouter models continue to use the
`openrouter/provider/model` form.
See [Bring your own model account (BYOK)](/agents/byok) for setup, project
enablement, billing behavior, and troubleshooting.
# Serverless Agents
Source: https://docs.opencomputer.dev/agents/overview
Define reactive agents in TypeScript and run them in the OpenComputer cloud
OpenComputer is a managed runtime for agents defined as code. A project can
contain several agents. You write the behavior; OpenComputer manages development deployments, model execution,
tools, durable sessions, streaming, and production versions.
```tsx theme={null}
import { useInput, useModel } from "@opencomputer/agent";
export default function Agent() {
const input = useInput();
useModel("anthropic/claude-sonnet-4.6");
return input.text
? `Help the user with this request: ${input.text}`
: "You are a helpful assistant.";
}
```
The exported function is a reactive description of the agent. OpenComputer
renders it for the current input and uses its return value as instructions for
the managed agent loop. Hooks attach the model, tools, and MCP servers
needed for that render. [Memory](/agents/memory) gives an agent
notes that outlive a session: project-scoped documents read at render, saved
by the model through memory tools, and owned by you.
> **[Projects](/agents/projects)**
>
> Group multiple agents in one repository.
> **[Reactive agents](/agents/reactive-agents)**
>
> Build agent behavior with a function and conditional hooks.
> **[Deployments](/agents/deployments)**
>
> Move immutable agent versions through Development and Production.
> **[Sessions](/agents/sessions)**
>
> Stream responses and inspect durable conversations.
> **[Build an application](/agents/react)**
>
> Create sessions on your server and attach a React interface.
> **[Capabilities](/agents/capabilities)**
>
> Choose between tools, MCP servers, and skills.
> **[Secrets](/agents/secrets)**
>
> Declare secret-backed connections without exposing credentials to agents.
> **[Logs](/agents/logs)**
>
> Follow agent and outbound-request activity from the CLI.
## Start a project
```bash theme={null}
npx @opencomputer/cli init my-agent
cd my-agent
npm install
npx --package @opencomputer/cli opencomputer login
npx --package @opencomputer/cli opencomputer link --create-project "my-agent"
npm run deploy -- --watch
```
The initializer creates one hello-world agent. `link` connects the directory
to a cloud project; [link by ID or slug](/agents/projects#create-or-select-the-cloud-project)
to use an existing one. The watcher prints the dashboard URL and syncs every
configured agent to `development`.
> [!NOTE]
>
> There is no local agent server. Agent code runs in OpenComputer's managed
> development cloud.
## What is available today
* TypeScript projects containing one or more agents
* reactive instructions, model selection, tools, MCP
servers, input metadata, session data reads, and document memory
* cloud development with file watching and automatic synchronization
* dashboard and CLI playground sessions
* immutable deployments promoted through aliases such as `production`
* project-level secrets with optional agent overrides and constrained outbound requests
* indexed runtime and egress logs available from the CLI
Agent behavior belongs in the repository under `opencomputer/`. The dashboard
is primarily for selecting environments, testing agents, and inspecting what
was deployed.
# Playground and debugging
Source: https://docs.opencomputer.dev/agents/playground
Test deployments and inspect what the agent received for each turn
The agent playground is the fastest way to test a cloud deployment without
building a client. Open a project, choose an environment and agent, then create
a session.
## Select the correct target
The project header selects `development` or `production`. Projects with
multiple agents have a separate agent selector in the playground. A session
keeps the deployment selected when it starts.
Create a new session after changing the target when you want an isolated test.
Resume an existing session when you want to verify multi-turn behavior.
## Use the debug inspector
The inspector shows the information needed to explain a turn:
* deployment and session identifiers
* rendered agent instructions
* selected model
* tools and MCP capabilities available to that render
* tool activity and errors
* workspace files the agent wrote under `/workspace`, with verified download of
a single file or the whole workspace as a zip (see
[Workspace files](/agents/sessions#workspace-files))
Newly written workspace files appear gradually and may take a few moments to
finish syncing. Use the refresh action when a file has just been created.
OpenComputer's private platform instructions and credentials are intentionally
not shown. The inspector describes your deployed agent configuration, not the
internal execution implementation.
## Follow runtime logs
Use the CLI when a failure needs more detail:
```bash theme={null}
npx --package @opencomputer/cli opencomputer logs --follow
npx --package @opencomputer/cli opencomputer logs --session
```
See [Logs](/agents/logs) for filters and JSON output.
# Projects and agents
Source: https://docs.opencomputer.dev/agents/projects
Organize multiple cloud agents in one project
A project is the cloud boundary for related agents, environments, deployments,
and sessions. A source repository contains the project's agent definitions.
## Project structure
The starter is intentionally small:
```text theme={null}
my-agent/
├── opencomputer/
│ ├── .env.example
│ ├── project.ts
│ ├── database/ # optional default database migrations
│ └── agents/
│ └── hello-world/
│ ├── agent.ts
│ ├── tools/ # optional
│ ├── skills/ # optional
│ └── schedules/ # optional
└── package.json
```
* `opencomputer/project.ts` declares project metadata and agent IDs.
* `opencomputer/agents//agent.ts` defines one agent.
* `opencomputer/database/migrations/` holds ordered SQL migrations for the
project's [default database](/agents/database).
* `opencomputer/.env.local` optionally holds ignored development secrets.
* `.opencomputer/project.json` records the linked cloud project; the CLI's
build output is cached under `node_modules/.cache/opencomputer/` and never
lands in `opencomputer/`.
Add source files only when the project uses them. The starter does not generate
placeholder capability directories.
## Create or select the cloud project
The source scaffold and cloud project are separate. `opencomputer init` writes
local source. Link it to a cloud project with:
```bash theme={null}
npx --package @opencomputer/cli opencomputer link --project
# Or create one explicitly:
npx --package @opencomputer/cli opencomputer link --create-project "Support agents"
```
There is no project picker. If you skip linking, project-scoped commands return
a structured `binding_required` error with the explicit command to run.
You can also link as part of a watched deploy:
```bash theme={null}
npx --package @opencomputer/cli opencomputer deploy --watch --project
npx --package @opencomputer/cli opencomputer deploy --watch --create-project "Support agents"
```
Later commands reuse `.opencomputer/project.json`. Run `opencomputer link` with
an explicit selector again when you intentionally want this source directory
to target another project.
## Add another agent
Create a new agent module:
```text theme={null}
opencomputer/agents/researcher/agent.ts
```
```tsx theme={null}
import { useModel } from "@opencomputer/agent";
export default function Agent() {
useModel("anthropic/claude-sonnet-4.6");
return "Research the request, verify claims, and cite useful sources.";
}
```
Then list it in `opencomputer/project.ts`:
```tsx theme={null}
export default {
name: "Customer workspace",
agents: ["hello-world", "researcher"],
};
```
The running development process synchronizes both agents. In the dashboard,
the project-level environment selector switches between `development` and
`production`; the playground has a separate agent selector.
## Inspect the project
The current project dashboard provides:
* **Agent playground** for creating and resuming test sessions
* **Deployments** for immutable version history and active aliases
* **Sessions** created through the dashboard, React client, or API
* **Schedules** deployed from each agent's source directory
* **Memory** for reading, editing and exporting saved agent notes
* **Database** for inspecting tables, rows and applied migrations, and running
read-only queries
* **Secrets** for write-only project and agent credentials
Project and agent behavior remains code-owned. The dashboard is for selection,
inspection, and testing. Use `opencomputer logs` to diagnose agent and
outbound-request failures.
## Archive and restore a project
Archive a project from the Projects page when you no longer want it active.
Archiving stops running sessions and prevents new deployments, sessions,
schedules, Slack turns, and webhook deliveries. It preserves the project's
agents, deployment history, secrets, connections, and other configuration.
Archived projects remain available in the **Archived** section. Select
**Restore** to return one to the active project list with its configuration
intact.
# Quickstart
Source: https://docs.opencomputer.dev/agents/quickstart
Create an agent project and sync it to Development (Cloud)
## Prerequisites
* Node.js 22 or newer
* an OpenComputer account
## 1. Create the agent
```bash theme={null}
npx @opencomputer/cli init my-agent
cd my-agent
npm install
```
Use `.` instead of `my-agent` to initialize an empty current directory. The
initializer creates one hello-world agent.
## 2. Log in
```bash theme={null}
npx --package @opencomputer/cli opencomputer login
```
The CLI opens the OpenComputer device-login flow and stores the login locally.
The production API at `https://app.opencomputer.dev` is used by default.
## 3. Watch development deployments
Link the directory to a new cloud project:
```bash theme={null}
npx --package @opencomputer/cli opencomputer link --create-project "my-agent"
```
To use an existing project, [link by its ID or slug](/agents/projects#create-or-select-the-cloud-project)
instead. Start the deployment watcher:
```bash theme={null}
npm run deploy -- --watch
```
The process prints the project dashboard URL, watches `opencomputer/`, and
deploys changes to `development`. Open the dashboard playground or use the CLI
to send a message.
The watcher stays open like a live-reload development process:
```text theme={null}
OpenComputer Development
✓ Deployment ready
Project my-agent (prj_...)
Agents hello-world@development
Dashboard https://app.opencomputer.dev/projects/prj_...
Watching /path/to/my-agent/opencomputer for changes.
Changes deploy automatically. Press Ctrl+C to stop.
```
> [!NOTE]
>
> This command does not start a local agent server or a browser application.
## 4. Change the agent
Edit `opencomputer/agents/hello-world/agent.ts`:
```tsx theme={null}
import { useInput, useModel } from "@opencomputer/agent";
export default function Agent() {
const input = useInput();
useModel("anthropic/claude-sonnet-4.6");
return `Be concise and practical. Current request: ${input.text ?? "none"}`;
}
```
Saving the file publishes a new development deployment. Start a new playground
or client session to test it; an existing session keeps the deployment it
started with.
See [Projects](/agents/projects) for project selection and
[Secrets and runtime variables](/agents/secrets) for automatic development
secret synchronization.
## 5. Deploy to production
```bash theme={null}
npm run deploy -- --alias production
```
The command builds an immutable deployment and advances the `production`
alias. Development remains available for subsequent edits.
See [Deployments and environments](/agents/deployments) for aliases and
version history.
> **[Projects](/agents/projects)**
>
> Add more agents and understand project bindings.
> **[Reactive agents](/agents/reactive-agents)**
>
> Add models, tools, and MCP servers.
# React integration
Source: https://docs.opencomputer.dev/agents/react
Create sessions on your server; attach, submit work and reopen them with useAgent
`@opencomputer/react` provides `useAgent`: a session's conversation, turn
activity and input controls. Your server creates sessions and checks access;
the hook reads their event log and sends turns through your routes. The API
key stays on the server.
```bash theme={null}
npm install @opencomputer/react
```
## Create the session on your server
Use the [management client](/reference/typescript-sdk/agents) from trusted
code. Address the agent by its environment alias, with one key per
submission:
```ts theme={null}
import { OpenComputer } from "@opencomputer/sdk/agents";
const oc = new OpenComputer({ apiKey: process.env.OPENCOMPUTER_API_KEY! });
const { session } = await oc.sessions.create(
{ agentId: "@development", labels: { request: taskId } },
{ idempotencyKey: taskId },
);
// Return session.id to the browser; use it when reopening this conversation.
```
`taskId` belongs to one submission. The platform chooses the deployment
active in the environment and records it on the session, so retry an
uncertain create with the same parameters and key: it returns the same
session even after a redeploy. Nothing in your application handles
deployment ids; pinning one is an advanced option of `sessions.create`.
Creating a session and sending its first turn are separate calls with
separate keys.
## Expose three routes
The hook needs these [management API](/agents/api) routes, relative to its
`basePath`:
| Route | Purpose |
| - | - |
| `GET /sessions//events?after=` | Read conversation history and new events |
| `POST /sessions//turns` | Submit `{ input, idempotencyKey, payload?, answers? }` |
| `POST /sessions//interrupt` | Request Stop for the running turn |
Authenticate each request and check access to the session before forwarding
it with your organization API key. For example, a Next.js route handler:
```ts theme={null}
// app/api/agent/sessions/[id]/[action]/route.ts
const allowed: Record = {
GET: ["events"],
POST: ["turns", "interrupt"],
};
async function proxy(
request: Request,
{ params }: { params: Promise<{ id: string; action: string }> },
) {
const { id, action } = await params;
const user = await requireUser(request);
if (!allowed[request.method]?.includes(action) || !(await userOwnsSession(user, id))) {
return new Response(null, { status: 404 });
}
const upstream = await fetch(
`https://app.opencomputer.dev/api/managed-agents/sessions/${encodeURIComponent(id)}/${action}${new URL(request.url).search}`,
{
method: request.method,
headers: {
"x-api-key": process.env.OPENCOMPUTER_API_KEY!,
"content-type": "application/json",
},
body: request.method === "POST" ? await request.text() : undefined,
redirect: "error",
},
);
return new Response(upstream.body, {
status: upstream.status,
headers: { "content-type": "application/json" },
});
}
export const GET = proxy;
export const POST = proxy;
```
`requireUser` and `userOwnsSession` are your application's access checks.
The latter must enforce the application's project, environment and agent
scope as well as the user's access. Forward JSON bodies and response statuses
unchanged: the hook supplies the turn key in the body and reads the admission
receipt or error envelope from the response.
## Attach and submit
Keep each submission's text and key until its receipt arrives. If the reply
is lost, Retry sends the same submission; it must not generate a new key.
```tsx theme={null}
import { useState } from "react";
import { useAgent } from "@opencomputer/react";
type Submission = { text: string; idempotencyKey: string };
export function Chat({ sessionId }: { sessionId: string }) {
const { messages, turns, send, stop, isReplaying, error } = useAgent({
sessionId,
basePath: "/api/agent",
});
const [draft, setDraft] = useState("");
const [pending, setPending] = useState(null);
const [sending, setSending] = useState(false);
async function submit() {
const submission = pending ?? {
text: draft.trim(),
idempotencyKey: crypto.randomUUID(),
};
setPending(submission);
setSending(true);
try {
await send(submission.text, { idempotencyKey: submission.idempotencyKey });
setPending(null);
setDraft("");
} catch {
// The hook exposes the error. Keep this submission for retry.
} finally {
setSending(false);
}
}
return (
{isReplaying ?
Loading the conversation…
: null}
{messages.map((message) => (
{message.role}: {message.text}
))}
setDraft(event.target.value)}
/>
{error ?
{error}
: null}
);
}
```
This example retains the submission for the life of the component. To retry
across navigation or a reload, retain the same envelope there too. Include
`payload` in it when sending structured input. An explicit rejection such as
`session_ended` requires resolving that error; a new key starts new work and
is not recovery of an uncertain request.
Turns sent while another runs are queued. `send` resolves on admission, not
when the agent finishes. It returns `{ sessionId, turnId, status, duplicate }`:
* A new turn is `queued` or `running`.
* A repeated key returns the existing turn and its current status, including
`completed`, `failed` or `cancelled`, with `duplicate: true`.
* Keep both the text and payload unchanged when retrying a key.
The input appears in `messages` after admission. Its event-log record
confirms that message instead of adding a second one.
### Send options and errors
| Option | Purpose |
| - | - |
| `idempotencyKey` | One key per submission, reused on retries. Without it, **each call** generates a new key and can admit a new turn. |
| `payload` | Structured JSON available to the agent as `useInput().payload`, beside the text |
| `answers` | The id of the open question this input answers; `answer()` sets it |
`send` rejects with a `SendError`. Rejection alone does **not** establish
whether a turn was admitted:
| `code` | Meaning | `status` |
| - | - | - |
| The API's code, such as `session_ended`, `memory_admission_unconfirmed` or `insufficient_credits` | An error response; follow that code's [recovery contract](/agents/api#turns) | HTTP status |
| `network_error` | Transport or response-reading failure; admission may have succeeded | none |
| `invalid_response` | The response was not a turn receipt; admission may have succeeded | none |
| `empty_input` | Blank text; nothing sent | none |
| `busy` | Create mode only: a turn already runs; nothing sent | none |
The hook also puts the message in `error`. A turn that fails after admission
is separate: `turn.failed` sets the turn's `failure` and the hook's `error`.
## Display activity and results
`turns` comes from the same durable event log as `messages`. Use its statuses
to distinguish queued work from running and settled turns:
```tsx theme={null}
{turns.map((turn) => (
{turn.input} — {turn.status}
{turn.toolCalls.map((call) => (
{call.title}: {call.status}
))}
{turn.result !== undefined ?
{JSON.stringify(turn.result, null, 2)}
: null}
{turn.failure ?
{turn.failure.message}
: null}
))}
```
| Turn field | Meaning |
| - | - |
| `id`, `status`, `input` | Turn ID, lifecycle status and admitted text |
| `messages` | This turn's messages, also present in the hook's `messages` |
| `toolCalls` | `{ callId, tool, title, input?, output?, status }` in start order; values are decoded JSON |
| `result` | This turn's latest committed [result-tool output](/agents/tools#the-session-result), if any; ordinary tool output does not become a result |
| `failure` | `{ code, message }` from `turn.failed` |
| `outcome` | `"question"` when the turn completed by asking |
Reporting a result does not finish a turn. The session's latest result may
also belong to an earlier turn; see [result provenance](/agents/sessions)
before treating a session as ready for review.
A tool call is `running`, `completed`, `failed` or `cancelled`. Calls still
running when their turn settles are settled too. The hook exposes failure
status but does not currently retain `tool.failed.message` or `settledBy` on
`ToolCall`; those details remain in the [event log](/agents/events#tools),
also available through `onEvent`. Older tool records may lack a name or
output.
`isRunning` is an activity hint, not the session's lifecycle status. It also
includes this hook instance's unsettled admission receipts. After reopening,
a queued turn is visible in `turns` but does not by itself set `isRunning`.
Use `turns` for queued/running counts and the [session API](/agents/api#sessions)
for session status.
## Answer a question
An agent can end a turn by [asking a question](/agents/tools#ask-a-question).
The hook exposes the open question as `question`, `{ id, text, options,
turnId? }`, or `null`, and `answer(questionId, text, options?)` sends the
reply. The asking turn's `outcome` is `"question"`.
```tsx theme={null}
const { question, answer } = useAgent({ sessionId, basePath: "/api/agent" });
{question ? (
{question.text}
{question.options.map((option) => (
))}
) : null}
```
`answer(id, text, options)` is `send(text, { ...options, answers: id })`
and resolves with the same receipt. `options` is empty for a free-text
question; answer it with the person's text. `question` clears when the
log records `question.answered` or `question.closed` for it. Answering a
question that is no longer open rejects with a `SendError` whose code is
`question_stale`; the hook's `question` shows the current one.
While a question is open, `send` without `answers` resolves with a held
receipt, `{ status: "held", questionId }` and no `turnId`: the message stays
in `messages`, rebuilt from the log's `message.held` events after a reload,
and reaches the agent with the answer. `dismiss(questionId)` closes the
question without an answer; held messages then run as ordinary turns.
## Stop and reopen
`stop()` requests an interrupt without spending a model turn. It resolves
once the request was accepted and rejects with a `SendError` when it was
not, after setting `error`, so a control that showed a stopping state can be
re-enabled and the request retried; a resolved promise is **not**
confirmation that execution has stopped. Watch the running
turn settle in `turns`. Stop affects that turn, so the next queued turn can
start afterwards. The [interrupt contract](/agents/api#end-and-interrupt)
describes settlement and remote-command guarantees.
The hook does not expose `stopping` or `ended` session status. Read the
session through your server when your interface needs that lifecycle detail.
It also never suspends, resumes or ends an attached session; those operations
belong to the server that created it.
Reopen by mounting the hook with the same `sessionId`. It reads history from
`after` (default `0`), including work completed while the page was closed,
then polls for new events. A failed poll sets `error`, backs off and resumes
from the same cursor; a successful recovery clears that polling error.
Changing `sessionId` replays the new session. A send still in flight for the
previous session settles for its caller without changing the new session's
view. Remount your composer for the new session, for example
``, so a retained submission
cannot be retried into another session.
`after` skips earlier events; it is not a snapshot. Start at `0` to reconstruct
the conversation and turn activity. Use another cursor only when the earlier
view is already retained elsewhere. See [Sessions and turns](/agents/sessions)
for the distinction between reopening a conversation and recovering failed
execution.
## Bind memory
Choose [memory bindings](/agents/memory) during server-side session creation;
the browser cannot change them. To create a document and bind it in one
helper call, see [`startOnDocument`](/reference/typescript-sdk/agents#start-a-session-on-a-memory-document)
and its retry limits. For pinned session creation, use the explicit
[document and session calls](/agents/document-memory#start-a-session-on-a-new-document).
Pass `onMemorySaved` to refresh your document panel through an authenticated
owner route:
```tsx theme={null}
const agent = useAgent({
sessionId,
basePath: "/api/agent",
onMemorySaved: () => refreshNotes(),
});
```
`memorySaves` also contains those events, with `resource`, `documentId`,
`revision` and `bytes`. Save events are best-effort; refresh on opening the
panel too, because documents can change without an event in this session.
## Local development
For a local application, pass `agent-id@alias` instead of a session ID:
```tsx theme={null}
const { messages, send, sessionId, isRunning, error } = useAgent(
"hello-world@development",
);
```
The first `send` creates a session through the authenticated development
bridge. Later calls resume that session; each call streams a turn and then
suspends it. Unlike attach mode, it normally waits through the turn and suspension
before resolving. Failures after admission appear through `error`; the
returned receipt still describes admission, not successful completion. Created sessions
have no memory bindings, and the hook does not recover their identity across
a page reload. Use attach mode for an application users will reopen.
Run the watched agent deployment and web application separately:
```bash theme={null}
npm run deploy -- --watch
npm run dev:web
```
The first provides the development bridge at the default `basePath`; the
second starts the web application. Credentials are not bundled into browser
code.
## Hook reference
| Value | Purpose |
| - | - |
| `messages` | User and assistant messages in log order |
| `turns` | Turns with status, input, messages, tool calls, result and failure |
| `send(text, options?)` | Submit a turn; resolve with a receipt or reject with `SendError` |
| `stop()` | Request an interrupt; rejects with `SendError` on a failed request, settlement arrives in `turns` |
| `question` | The open question, `{ id, text, options, turnId? }`, or `null` |
| `answer(questionId, text, options?)` | Answer the open question; same receipt and errors as `send` |
| `dismiss(questionId)` | Close the open question without an answer |
| `sessionId` | Attached session, or the created one after the first send |
| `isRunning` | Running activity or this instance's unsettled admissions; not a session status |
| `isReplaying` | Attach mode: existing history is still loading |
| `error` | Latest reported request or turn error |
| `memorySaves` | Observed `memory.saved` events |
| `cursor` | Last applied event position |
| Option | Purpose |
| - | - |
| `sessionId` | Attach to an existing session |
| `agent` | Create mode: `agent-id@alias` |
| `basePath` | Route prefix; default `/api/opencomputer/managed-agents` |
| `fetch` | Custom fetch, for example to add a CSRF header |
| `after` | Attach mode: skip events through this position; default `0` |
| `pollIntervalMs` | Attach mode: empty-poll interval; default 1000 ms, 500 ms while running |
| `onEvent` | Every applied event, including replayed history |
| `onMemorySaved` | Each observed `memory.saved` event |
| `source` | Create mode: input source |
# Reactive agents
Source: https://docs.opencomputer.dev/agents/reactive-agents
Render instructions and capabilities from the current input
An agent is a synchronous TypeScript function that returns instructions. The
OpenComputer runtime owns the durable conversation, streaming model calls,
tool loop, and continuation. Your function calculates the agent configuration
for the current render.
```tsx theme={null}
export default function Agent() {
return "You are a helpful assistant.";
}
```
This keeps ordinary TypeScript control flow available. Hooks may be called
conditionally, so a capability can be attached only when the current request
needs it.
## Hooks
| Hook | Current value or effect |
| - | - |
| `useInput()` | Reads the current source, text, and optional structured payload |
| `useCurrentInput()` | Alias for `useInput()` |
| `useModel(model)` | Selects a model using `provider/model` notation |
| `useTool(tool)` | Makes a named or code-defined tool available |
| `useMcpServer(server)` | Attaches a managed HTTPS MCP server |
| `useSessionData(key)` | Reads a value previously stored in the current session |
| `useMemory(memory)` | Reads bound memory and selects its permitted tools |
`useInput()` is not a human-in-the-loop prompt. It describes the input that
caused the current render. See [Inputs](/agents/inputs) for its text, payload,
and source metadata.
## Conditional configuration
```tsx theme={null}
import {
useInput,
useModel,
useTool,
} from "@opencomputer/agent";
export default function Agent() {
const input = useInput();
useModel("anthropic/claude-sonnet-4.6");
if (input.text?.includes("research")) {
useTool("web-search");
}
return "Answer directly. Research and verify when those capabilities exist.";
}
```
The hook calls describe capabilities for the next model step; they do not run
the model or tool themselves.
## Define a tool
Tools are TypeScript values with JSON Schema inputs and an asynchronous or
synchronous `run` function. Put the implementation beside the agent, for
example at `opencomputer/agents/hello-world/tools/hacker-news.ts`:
```tsx theme={null}
import { defineTool } from "@opencomputer/agent";
export const latestStories = defineTool({
name: "latest_hacker_news_stories",
description: "Fetch the current top Hacker News stories",
input: {
type: "object",
properties: { limit: { type: "number" } },
},
async run({ input, signal, reportProgress }) {
await reportProgress({ status: "fetching" });
const limit = Number(input.limit ?? 10);
const ids = await fetch(
"https://hacker-news.firebaseio.com/v0/topstories.json",
{ signal },
).then((response) => response.json() as Promise);
return ids.slice(0, limit);
},
});
```
Import and attach it from `agent.ts`:
```tsx theme={null}
import { useTool } from "@opencomputer/agent";
import { latestStories } from "./tools/hacker-news";
export default function Agent() {
useTool(latestStories);
return "Use the Hacker News tool when the user asks for current stories.";
}
```
Tool names may contain letters, numbers, underscores, and hyphens. Tool code
runs in the managed agent runtime, not in the browser.
## Skills
Skills are reusable instruction bundles. Add a skill only when an agent needs
one:
```text theme={null}
opencomputer/agents/hello-world/skills/code-review/SKILL.md
```
Skills in an agent's `skills/` directory are packaged with that agent and can
be loaded by the runtime when their description matches the task. Unlike
tools, skills provide instructions and supporting resources rather than a
callable JSON Schema function.
## Models
Use `provider/model` strings:
```tsx theme={null}
useModel("anthropic/claude-sonnet-4.6");
```
An object form is also supported:
```tsx theme={null}
useModel({ provider: "anthropic", model: "claude-sonnet-4.6" });
```
Provider credentials are managed by OpenComputer and are not placed in agent
source or the React application.
Continue with [Agent hooks](/agents/hooks), then use the focused guides for
[inputs](/agents/inputs), [models](/agents/models), [tools](/agents/tools),
[MCP servers](/agents/mcp), [skills](/agents/skills), and
[session data](/agents/session-data).
# Regions
Source: https://docs.opencomputer.dev/agents/regions
Run an agent's sandbox and session state closer to your users
By default, an agent's sandbox runs wherever capacity is available, usually in
the United States. To run sessions closer to your users or systems, set
`region` in the agent's `opencomputer.toml` and deploy again:
```toml theme={null}
schema = 1
id = "support"
name = "Support"
region = "au"
```
## Allowed values
`region` takes one of the values below.
OpenComputer places the agent's sandbox in that region, and creates session
state in the nearest Cloudflare location:
| `region` | Sandbox runs in | Session state placed near |
| - | - | - |
| omitted | wherever capacity is available, usually the United States | the first request |
| `"us"` | anywhere in the United States | eastern North America |
| `"eu"` | anywhere in the European Economic Area | western Europe |
| `"ap"` | anywhere in Asia-Pacific | Asia-Pacific |
| `"us-east"`, `"us-central"`, `"us-south"` | that part of the United States | eastern North America |
| `"us-west"` | western United States | western North America |
| `"ca"` | Canada | eastern North America |
| `"mx"` | Mexico | western North America |
| `"eu-west"`, `"eu-south"` | that part of Europe | western Europe |
| `"eu-north"` | northern Europe | eastern Europe |
| `"uk"` | United Kingdom | western Europe |
| `"ap-northeast"`, `"ap-southeast"`, `"ap-south"` | that part of Asia-Pacific | Asia-Pacific |
| `"jp"` | Japan | Asia-Pacific |
| `"au"`, `"ap-melbourne"` | Australia | Oceania |
| `"me"` | Middle East | Middle East |
| `"sa"` | South America | South America |
| `"af"` | Africa | Africa |
A broad region (`us`, `eu`, `ap`) lets the sandbox run anywhere inside it, so
it starts sooner when capacity is tight. A narrow region pins it more
precisely.
* Values are lowercase and exact. `"AU"` and cloud zone names such as
`"ap-southeast-2"` or `"us-east-1"` are rejected.
* The value must be a quoted string. `region = au` is rejected.
* To go back to the default, remove the `region` line and deploy again.
An unsupported value stops the deploy before anything is uploaded:
```text theme={null}
opencomputer.toml region "us-east-1" is not supported; use one of: us, eu, ap, us-east, …
```
The API rejects it too, with `400 unsupported_region`, if you call
`POST /v1/deployments` directly.
## What the region covers
* **Sandbox.** The agent's sandbox, where `sandbox_exec` and other tools run,
is created in the region. Its outbound traffic leaves from that region too.
* **Session state.** Session state is created near the region. This placement
is best effort.
* **Not moved.** Account-level data, such as project and deployment records,
stays where it is.
## Changing the region
A region change applies to sessions started after the deploy. Existing
sessions stay where they started. Each agent in a project has its own
`opencomputer.toml`, so agents in one project can run in different regions.
## Cold starts
The first session after you deploy to a new region is slower to start. The
agent's sandbox image has to be pulled into that region, and the first tool
call can take a few minutes. Later sessions reuse the cached image and start
much faster. To take the cold start before your users do, start one session
that runs a command after the deploy. The sandbox starts only on the first tool
call, so a session that only chats does not warm it:
```sh theme={null}
opencomputer session "Run: echo ready"
```
# Schedules
Source: https://docs.opencomputer.dev/agents/schedules
Run an agent from a code-defined cron schedule
Schedules start durable agent sessions at recurring times. Their timing,
timezone, target agent, and input are defined in source control and deployed
with the project.
## Define a schedule
Create a TypeScript file under an agent's `schedules/` directory. The filename
must match the schedule ID:
```text theme={null}
opencomputer/
└── agents/
└── feature-flag-hygiene/
├── agent.ts
└── schedules/
└── weekday-hygiene.ts
```
```ts weekday-hygiene.ts theme={null}
import { defineSchedule } from "@opencomputer/agent";
export default defineSchedule({
id: "weekday-hygiene",
cron: "0 9 * * 1-5",
timezone: "America/Los_Angeles",
enabled: ["production"],
overlap: "skip",
dispatch: {
text: "Review stale feature flags.",
payload: {
mode: "async",
repository: "acme/widgets",
dryRun: true,
},
},
});
```
Schedules use five-field cron expressions. `timezone` is an IANA timezone such
as `UTC`, `America/Los_Angeles`, or `Europe/London`. The dispatch payload must
be JSON-compatible and must not contain secrets.
## Where schedules run
When `enabled` is omitted, it defaults to `["production"]`.
In a single-environment project, a schedule that enables `production` (or
`default`) runs automatically; one that enables only `development` is
**Manual only**.
### Legacy Development and Production projects
* Development displays every deployed schedule and supports **Run now**.
Schedules that do not enable Development are marked **Manual only** and never
recur there.
* Production automatically runs schedules that enable Production.
* Add `"development"` to `enabled` only when you intentionally want automatic
Development recurrence.
Development and Production have separate schedule state, run history,
sessions, secrets, and database state.
## Receive scheduled input
Each run starts a fresh session pinned to the deployment that was active when
the run was claimed. `useInput()` includes the static dispatch and execution
metadata:
```tsx theme={null}
import { useInput } from "@opencomputer/agent";
export default function Agent() {
const input = useInput();
const payload =
input.payload &&
typeof input.payload === "object" &&
!Array.isArray(input.payload)
? input.payload
: {};
if (payload.mode === "async") {
return "Complete the unattended hygiene workflow and publish its result.";
}
return "Ask the user which repository they want to inspect.";
}
```
Use payload fields such as `mode` to select business behavior. The platform
sets `input.source` to `"schedule"` as provenance, but transport provenance
should not be the agent's business-mode switch.
After narrowing `input.source === "schedule"`, `input.schedule` contains the
schedule ID, run ID, intended time, timezone, attempt number, and whether the
run was started manually.
## Overlap and run history
`overlap` defaults to `"skip"`. If an earlier run is still starting or running,
the next occurrence is recorded as skipped. Use `"allow"` only when concurrent
runs are safe.
The project's **Schedules** tab shows the next and previous run, current
activation state, recent outcomes, and linked sessions. **Run now** uses the
same deployment and dispatch payload as an automatic occurrence.
Schedule timing and payload remain code-owned. Redeploy the project after
changing a definition.
# Secrets and runtime variables
Source: https://docs.opencomputer.dev/agents/secrets
Configure managed outbound credentials and agent runtime environment variables
OpenComputer secrets are write-only values used by declared outbound
connections. Plaintext values are not included in source bundles, prompts,
deployment manifests, runtime environment variables, logs, or API responses.
> [!NOTE]
>
> Some credentials are not secrets you set. When someone connects their Gmail,
> Calendar, Drive or Sheets account, the platform holds and refreshes that
> credential — see [Connected services](/agents/services).
## Declare a connection
```tsx theme={null}
import { bearer, defineConnection, useSecret } from "@opencomputer/agent";
const github = defineConnection({
id: "github-api",
origin: "https://api.github.com",
methods: ["GET"],
pathPrefix: "/repos/",
headers: {
Authorization: bearer(useSecret("GITHUB_TOKEN")),
},
});
export default function Agent() {
return "Use GitHub when it helps answer the request.";
}
```
The origin must use HTTPS. OpenComputer rejects hard-coded sensitive headers
such as `Authorization`, `Cookie`, and `X-API-Key`; reference a managed secret
instead.
Tools can make requests through the same connection:
```tsx theme={null}
const response = await github.fetch("/repos/opencomputer/example");
const repository = await response.json();
```
Only a relative path is accepted. OpenComputer checks the declared origin,
path prefix, method, agent, and environment before sending the outbound
request.
## Synchronize development secrets
Place development-only values in `opencomputer/.env.local`:
```dotenv theme={null}
GITHUB_TOKEN=github_pat_...
```
`opencomputer doctor` compares these local names with `useSecret()` references
and `.env.example`. Upload is always explicit; deployment never opens a value
prompt. The CLI infers the allowed origins from the source declaration.
Variables without a matching declaration are skipped. OpenComputer never
grants an unmatched value access to every host, and removing a local variable
does not delete its cloud value. Use `opencomputer secrets remove` when deletion
is intentional. The starter ignores `opencomputer/.env.local` and includes an
`opencomputer/.env.example` file for documenting required names without values.
## Set a project secret
Secrets belong to a cloud project. Link explicitly first with
`opencomputer link --project ` or `--create-project `.
```bash theme={null}
printf %s "$GITHUB_TOKEN" | npx --package @opencomputer/cli opencomputer secrets set GITHUB_TOKEN --value-stdin
```
The CLI accepts the value only from piped standard input. When run from an
initialized project it infers allowed origins from connections that reference
the secret.
Project secrets are available to declared connections in every agent in the
project. Create an agent-specific override when one agent needs a different
credential:
```bash theme={null}
printf %s "$GITHUB_TOKEN" | npx --package @opencomputer/cli opencomputer secrets set GITHUB_TOKEN --value-stdin --agent current
```
Secrets are separate for `development` and `production`:
```bash theme={null}
printf %s "$GITHUB_TOKEN" | npx --package @opencomputer/cli opencomputer secrets set GITHUB_TOKEN --value-stdin --environment production
npx --package @opencomputer/cli opencomputer secrets list --environment development
npx --package @opencomputer/cli opencomputer secrets remove GITHUB_TOKEN --environment development
```
List output contains metadata such as the name, scope, environment, and
allowed origins. Secret values are never returned.
## Agent runtime variables
Use an agent runtime variable when code or a command must receive a value as a
normal environment variable. Configure it in the **Agent runtime variables**
section of the project's Secrets page, or with the CLI:
```bash theme={null}
printf %s "$DATABASE_URL" | npx --package @opencomputer/cli opencomputer env set DATABASE_URL --value-stdin
npx --package @opencomputer/cli opencomputer env list --environment development
npx --package @opencomputer/cli opencomputer env remove DATABASE_URL
```
The CLI reads new values only from piped standard input. Runtime variables can apply to
the whole project or override one agent with `--agent current`, and development
and production values are separate. No declaration in agent source is needed.
OpenComputer stores these values encrypted and never returns them through the
dashboard or management API. A newly started agent runtime receives the
resolved values in its process environment, so agent code, tools, commands, and
child processes can read them. Because the agent can access the plaintext,
runtime variables are appropriate for personal-agent credentials such as
`DATABASE_URL`, but they do not provide the destination isolation of managed
secrets. Restart a running agent runtime after changing a value.
## Security guarantees
For managed secrets, OpenComputer resolves the declared connection and scoped secret for each
request. The credential is attached only after the destination, method, path,
agent, project, and environment have been validated. The value is never added
to the agent's source bundle, prompt, browser application, or logs.
# Connected services
Source: https://docs.opencomputer.dev/agents/services
Call Google services, GitHub, and Linear with a credential the platform holds and refreshes
Some credentials cannot be a secret you set. An OAuth token is short-lived,
belongs to a person rather than to your project, and has to be refreshed. So
the platform holds it: someone connects their account once, and your agent asks
for a service by name instead of a URL and a header.
The access token never enters your runtime — there is nothing to log, leak, or
hand to a model.
> [!NOTE]
>
> This is the other half of [declared connections](/agents/secrets). Use
> `defineConnection` when **you** hold the credential and it is a static secret.
> Use `callService` when the **platform** holds it and has to refresh it.
## 1. Connect an account
Do this first — the code below returns `404` until an account exists.
Each account is connected under a **label**, which is how your agent addresses
it. Use the CLI, or the API directly if you are scripting.
```bash CLI theme={null}
opencomputer connection add gmail --alias support
```
```bash API theme={null}
export OC_API_URL=https://app.opencomputer.dev
curl -X POST "$OC_API_URL/api/managed-agents/connections/google/link" \
-H "x-api-key: $OPENCOMPUTER_API_KEY" \
-H "content-type: application/json" \
-d '{"service":"gmail","label":"support"}'
```
Both return a link. Whoever opens it consents with their own account and it is
connected under that label — they never sign in to OpenComputer, and no
credential passes through your side.
The CLI then waits and prints `Connected` once the account is authorized, so
you know it worked. It does not open a browser: the account often belongs to
somebody else, and this runs on servers as readily as on a laptop. Send them
the link and leave it waiting, or press Ctrl-C — the connection is unaffected.
For minting several links at once, `--no-wait` prints the link and exits.
`--json` does the same and is the one to script against.
Omitting the label connects it as `default`, which is what the examples below
assume. Calls do not need a label when exactly one eligible account is
connected. When several accounts are eligible, pass the label to select one;
otherwise the call returns `409 connection_alias_required` and lists them.
The provider segment in the URL is the grant, not the service — `google` for
gmail, calendar, drive and sheets; `github` for GitHub; and `linear` for Linear.
`connection add` remains an alias for `connection connect`. If consent was
revoked or the connected account must change, start a fresh OAuth flow without
changing the logical connection or its project attachments:
```bash theme={null}
opencomputer connection reconnect support --service gmail
```
**Listing what is connected:**
```bash CLI theme={null}
opencomputer connection list
# support google connected ops@example.com 3f9c1e77-...
# label provider status account connection id
```
```bash API theme={null}
curl "$OC_API_URL/api/managed-agents/connections" \
-H "x-api-key: $OPENCOMPUTER_API_KEY"
```
## 2. Attach the account to a project
By default a connected account is personal: calls resolve it from the person
who started the session. That is useful in the playground, but channel sessions
such as Slack have a channel principal rather than the project owner's user
identity.
Attach a connected account when it should be the project's shared service
credential:
```bash theme={null}
opencomputer connection attach default --service linear --project current
opencomputer connection list --project current
```
The attachment applies to the whole project, including all of its agents and
environments. An agent can use it only when its deployed code declares the
service with `useService()`. The session principal is still retained as the
actor for session ownership and audit; it no longer chooses the credential.
Detach removes only the project grant. It does not disconnect the underlying
OAuth account:
```bash theme={null}
opencomputer connection detach default --service linear --project current
```
Removing the connection itself also removes its project attachments. Preview
inheritance follows the project's attachment; a preview does not create a
second OAuth connection.
> [!NOTE]
>
> A connection that is not yet `connected` is re-checked with the provider each
> time it is listed, so `pending` means what it says. A **`connected`** account
> is not re-checked: if someone revokes access at the provider, it keeps reading
> `connected` until the next call to it fails. Treat `connected` as "was working",
> not "is working".
**Disconnecting one.** The CLI takes the label or the id; the API takes the id
from the listing above, and the service rather than the provider. If a label is
ever ambiguous the CLI refuses rather than guessing — `--service ` picks
one.
```bash CLI theme={null}
opencomputer connection remove support
```
```bash API theme={null}
curl -X DELETE \
"$OC_API_URL/api/managed-agents/connections/google?service=gmail&connectionId=$ID" \
-H "x-api-key: $OPENCOMPUTER_API_KEY"
```
Every CLI command takes `--json` if you want to parse its output.
## 3. Declare the service
```tsx theme={null}
import { useService, useTool } from "@opencomputer/agent";
import { unread } from "./tools/mail.js";
export default function Agent() {
useService("gmail");
useTool(unread);
return "Answer questions about the triage queue.";
}
```
`useService` takes a literal string, read out of your source at build time. It
puts the service in the capability manifest, so what your agent can reach is
reviewable without running it, and it asks the deployment for that grant —
without it, `listServices()` returns nothing.
| Service | Reaches | Grant |
| - | - | - |
| `gmail` | `gmail.googleapis.com` | Google |
| `calendar` | `www.googleapis.com` | Google |
| `drive` | `www.googleapis.com` | Google |
| `sheets` | `www.googleapis.com` | Google |
| `github` | GitHub's API | GitHub |
| `linear` | `api.linear.app/graphql` | Linear |
The Google services share one grant; GitHub and Linear each have their own. A name outside that
list fails the build rather than the request.
> [!NOTE]
>
> There are two ways to reach GitHub, and they are for different jobs. Use
> `callService({ service: "github" })` to call the REST API on an account someone
> connected. Use a [GitHub App connection](/agents/github) when the agent needs
> to run `git` and `gh` itself — that one injects a token into the sandbox rather
> than proxying requests.
## 4. Call it from a tool
`useService` goes in the agent; `callService` goes in a tool. An agent renders
synchronously and cannot wait for a request.
```tsx theme={null}
import { callService, defineTool } from "@opencomputer/agent";
export const unread = defineTool({
name: "unread_reports",
description: "Count unread messages labelled triage",
async run({ signal }) {
const response = await callService({
service: "gmail",
path: "/gmail/v1/users/me/messages?q=label:triage+is:unread",
signal,
});
if (!response.ok) {
throw new Error(`gmail: ${response.status} ${await response.text()}`);
}
const { messages = [] } = (await response.json()) as {
messages?: unknown[];
};
return { waiting: messages.length };
},
});
```
Note what that counts. Gmail returns at most 100 message ids per page, so
`messages.length` is the size of the first page, not the total — real counting
means following `nextPageToken`. Most collection APIs behave this way.
`callService` returns the service's own response — status, headers and body — as
`fetch` would, so a `403` for a missing scope stays distinguishable from a `404` for a
deleted message. Check `response.ok`; a model told only "it failed" will retry
forever.
Writes take the same shape:
```tsx theme={null}
await callService({
service: "gmail",
label: "support",
method: "POST",
path: "/gmail/v1/users/me/messages/abc/modify",
headers: { "content-type": "application/json" },
body: JSON.stringify({ removeLabelIds: ["UNREAD"] }),
signal,
});
```
Linear uses one GraphQL endpoint. Send a JSON GraphQL document with `POST`:
```tsx theme={null}
const response = await callService({
service: "linear",
method: "POST",
path: "/graphql",
headers: { "content-type": "application/json" },
body: JSON.stringify({
query: `query Viewer { viewer { id name email } }`,
}),
});
const result = (await response.json()) as {
data?: unknown;
errors?: Array<{ message: string }>;
};
if (!response.ok || result.errors?.length) {
throw new Error(result.errors?.[0]?.message ?? `linear: ${response.status}`);
}
```
Linear may report GraphQL errors in a successful HTTP response, so check both
`response.ok` and `errors`. The Linear connection accepts only `POST /graphql`;
other paths and methods fail at the broker.
### Linear in sessions started from Linear
Which credential a call uses follows an explicit order. A session started from
a [Linear agent connection](/agents/linear) always uses that connection's own
Linear app. Otherwise, a project-attached Linear account wins; when the project
has no Linear attachment, OpenComputer falls back to an eligible account owned
by the session principal. In a session started from a Linear agent connection,
the session belongs to the connection, and `service: "linear"` resolves to
that connection's own Linear app: the call acts as the agent, under the name
people delegate to, with the app's token attached by the platform.
* No person's connected Linear account is used in those sessions, and there
is no fallback to one; a `label` is ignored.
* The deployment must declare `useService("linear")`.
* Only `POST /graphql` is accepted, as everywhere.
The agent's code is the same in both kinds of session.
## 5. Several accounts
Accounts are connected and disconnected long after the artifact is built, so
ask for them rather than naming them in source.
```tsx theme={null}
export const sweep = defineTool({
name: "sweep_mailboxes",
description: "Count unread mail in every connected mailbox",
async run({ signal }) {
const mailboxes = await listServices({ provider: "google", signal });
const counts: Record = {};
for (const mailbox of mailboxes) {
const response = await callService({
service: "gmail",
label: mailbox.label,
path: "/gmail/v1/users/me/messages?q=is:unread",
signal,
});
const { messages = [] } = (await response.json()) as {
messages?: unknown[];
};
counts[mailbox.displayName ?? mailbox.label] = messages.length;
}
return counts;
},
});
```
| Field | What it is |
| - | - |
| `id` | The connection's id, for the disconnect route |
| `label` | The alias it was connected under. Pass this as `label`. |
| `displayName` | Who the account belongs to, e.g. the mailbox address |
| `provider` | `google`, `github`, or `linear` |
| `scopes` | What was granted at consent |
| `status` | `connected` accounts are usable |
Only connected accounts are returned by default; pass `connectedOnly: false`
for pending ones too. Connecting an account needs no redeploy — the next run
sees it, and a consent finished seconds ago is reconciled before the list is
answered.
## Limits
A proxied request is not an unrestricted `fetch`.
| | |
| - | - |
| Methods | `GET`, `POST`, `PUT`, `PATCH`, `DELETE`; Linear is `POST` only |
| Request headers | Only `accept`, `content-type`, `if-match`, `if-none-match` |
| Request body | 4 MB |
| Response body | 4 MB |
| Time | 30 seconds |
**Other request headers are dropped, not rejected.** The request is still sent
without them, so an API that changes behaviour on a header you set will act as
though you never set it. There is no error to catch.
Authentication headers are dropped for the reason they are on a declared
connection: the credential is the platform's to attach, and a request that
could overwrite it could also send it elsewhere.
Both size limits raise an error rather than truncating, so an unexpectedly
large response fails loudly instead of arriving half-parsed. Page through large
collections rather than asking for them in one call.
## When something fails
| You see | It means | Do this |
| - | - | - |
| `404 connection_not_found` | No eligible connected account exists, or no account matches the requested `label` | `opencomputer connection list` to inspect the accounts, then connect one or correct the label |
| `409 connection_alias_required` | Several accounts connected, no `label` given | Pass `label`; the message lists them |
| `403` from the service | The consent is narrower than the call needs | Reconnect the account with wider access |
| `listServices()` returns `[]` | The service is not declared | Add `useService("gmail")` |
| A connection stays `pending` | Nobody opened the link, or it expired | Run `connection reconnect` and send the new link |
| It works in the playground but not Slack | The account is personal, so the Slack principal cannot resolve it | Attach it with `connection attach --service --project current` |
| A `connected` account returns `401` | Access was revoked at the provider; the stored status has not caught up | Run `connection reconnect` with the same label |
| A header you set is ignored | It is not in the allowlist above | Nothing — it cannot be sent |
| Throws *managed connections are unavailable* | No connection layer | You are running outside the platform |
A `403` cannot be fixed from the agent. What a request may do is fixed by the
consent that created the connection; read `scopes` from `listServices()` to see
what was actually granted.
## What this does not do
The platform does not proxy arbitrary hosts. A service reaches its own API and
nothing else — for anything outside the table above, declare an
[HTTP connection](/agents/secrets) instead.
# Session data
Source: https://docs.opencomputer.dev/agents/session-data
Read durable per-session values with useSessionData
`useSessionData()` reads structured data associated with the current durable
session. Use it for application context that should influence later renders,
such as a campaign name, locale, audience, or workflow phase.
```tsx theme={null}
import { useSessionData } from "@opencomputer/agent";
export default function Agent() {
const campaign = useSessionData("campaign_name");
const audience = useSessionData("audience");
return [
"Help plan the campaign.",
campaign ? `Campaign: ${campaign}.` : undefined,
audience ? `Audience: ${audience}.` : undefined,
]
.filter(Boolean)
.join(" ");
}
```
## Read a value
The key is a string. Pass a TypeScript type argument for the expected value:
```tsx theme={null}
const locale = useSessionData("locale");
const approved = useSessionData("brief_approved");
const tags = useSessionData("tags");
```
The hook returns `undefined` when the key is absent. Handle that case rather
than assuming every session has been initialized.
Values must be JSON-compatible: `null`, booleans, numbers, strings, arrays,
and objects containing those values.
## Configure capabilities from session data
Because session data is available during render, it can control instructions
and resource hooks:
```tsx theme={null}
import { useSessionData, useTool } from "@opencomputer/agent";
import { publishCampaign } from "./tools/publish-campaign";
export default function Agent() {
const approved = useSessionData("brief_approved") ?? false;
if (approved) {
useTool(publishCampaign);
}
return approved
? "The brief is approved. Publishing is available when the user asks."
: "Draft and revise the brief. Do not publish it yet.";
}
```
## Session data versus input
`useInput().payload` belongs to the current admitted input. Session data is
associated with the durable session and can be read again on later renders.
Use input for the event that just arrived and session data for context that
outlives that event.
## Session data versus memory
Session data belongs to one session and is written by your application.
[Document memory](/agents/document-memory) belongs to the project and environment,
can be written by agents and the owner, and outlives any session. Use session
data for the current job and memory for notes shared with later sessions.
## Current write behavior
The current `@opencomputer/agent` API exposes session data as a read-only
snapshot. It does not yet include a hook for setting or updating a value from
agent code. Code should treat the returned value as immutable.
See [Sessions and turns](/agents/sessions) for the durable conversation
lifecycle and the [GTM engineer example](/agents/examples/gtm-engineer) for an
agent that continues user selections and drafting in one session.
# Sessions and turns
Source: https://docs.opencomputer.dev/agents/sessions
Understand durable conversations and multi-turn agent work
A session is a durable conversation with a deployed agent. Each turn appends
input and streamed runtime events to the session, allowing clients to resume a
conversation without rebuilding its history locally. The events are listed on
[Session events](/agents/events).
A session keeps the deployment it was created on, chosen by the platform
from the alias it was addressed to. Advancing Development or
promoting to Production changes which deployment new sessions get; turns sent
to an existing session keep running the code it started with, including its
declared tools and memory resources. To run new code, start a new session.
Memory documents are not pinned: they belong to the project and environment,
so a new session bound to the same document reads what the old one saved.
## Agent playground
Open a project in the dashboard and choose **Agent playground**. Select the
agent and environment independently, then create a new session or resume a
previous playground session.
Playground sessions stay in the playground list. Sessions started through an
API appear under **Sessions**, where their durable turn history can be
inspected.
See [Playground and debugging](/agents/playground) for target selection and the
debug inspector.
## Finding sessions
Give a session labels when you create it, such as the ticket, repository or
user it works for, and list sessions by project, environment, agent,
deployment, status, creation or update time and label instead of keeping
your own index of session ids. The list returns rows with the session's
status, its current turn activity and its latest result, and never the
prompts or messages; open a session for its turns and events. The query, the
row shape and the label bounds are on the [API
reference](/agents/api#get-and-list).
When one session stands for one record in your application, give it that
record's id as its `externalReference` at creation. The reference is stored
as given, returned on the session and its lifecycle events, and is an exact
list filter: after a lost create response, retry with the same request key
first, then `GET /sessions?externalReference=` finds the session
however many unrelated sessions have been created since. Retrying the key
with a different reference is refused, so a record cannot be attached to
the wrong session. Bounds and semantics are on the [API
reference](/agents/api#external-references).
The list is paged: at most 100 rows per request, 50 by default, newest
created first. Follow `nextCursor` with the same filters until it is
`null`; every session matching the filters appears exactly once, however
many were created in the same instant. The [TypeScript
client](/reference/typescript-sdk/agents#sessions) does this for you with
`sessions.iterate(filters)`.
## Execution, labels and results
These describe different parts of a session:
| Field | What it answers |
| - | - |
| `status` and `activity` | Is work queued, running, stopping or finished? |
| `labels` | How does your application organize the session? A ticket ID or `archived` label is your convention; changing it does not stop work. |
| `result` | What did the agent last report? Its `data` is validated against the [result tool's](/agents/tools#the-session-result) output schema; `turnId` and `callId` identify the report. |
| `question` | Is the agent waiting on a person? The open [question](#questions), or `null`. |
A report does not finish a turn, and each report replaces the whole result.
Later work can run, fail or be cancelled while an earlier result remains.
For a review queue, check current activity, the last settled turn's status,
and whether `result.turnId` matches that turn before treating the report as
ready. Your application decides what the reported data means.
## Questions
A turn can end by asking. When the agent calls the
[`ask`](/agents/tools#ask-a-question) tool, the turn completes with
`outcome: "question"` and the session's `question` is set:
```json theme={null}
{
"question": {
"id": "",
"text": "Plan first, or go ahead?",
"options": [{ "label": "Go", "value": "go" }, { "label": "Plan first", "value": "plan" }],
"askedAt": "2026-10-02T10:00:00.000Z"
}
}
```
`options` is empty for a free-text question. `question` is `null` when
nothing is open, and it is independent of `result`: a session can hold a
result and an open question at the same time. A consumer that only knows
completed turns still reads an asking turn correctly, because its status is
`completed`.
Answer by sending a turn that names the question with `answers`:
```ts theme={null}
await oc.sessions.turns.send(sessionId, {
input: "Go",
answers: session.question!.id,
idempotencyKey: "answer-1",
});
```
The agent reads the reply as [`useInput().answer`](/agents/inputs#answers-and-steering).
Naming a question that is not the open one is refused with
`409 question_stale`; read the session again and answer its current
question.
While a question is open, a turn sent without `answers` does not run on its
own. It is held, and delivered with the answer, in order, as `steering`, so
the turn that interprets the answer sees every constraint written in the
meantime. `steering` carries a bounded number of held inputs, up to 64 KiB;
the rest run as ordinary turns after the answer, in order. The API answers a
held input with `202 { status: "held", questionId }` and no `turnId`; see
[Turns](/agents/api#turns). A turn already queued behind the asking turn
settles as `cancelled` with reason `held` and keeps its turn id. Held inputs
are in the [event log](/agents/events#questions) as `message.held`, then
`message.delivered` or `message.discarded`.
One question is open at a time. The answering turn may ask again, which opens
a new question. A question closes without an answer with
`question.closed { reason }`:
| `reason` | Cause | Held inputs |
| - | - | - |
| `stopped` | The session was stopped from [Linear](/agents/linear#stop) | Discarded |
| `dismissed` | [Dismiss](/agents/api#questions) was called | Run as ordinary turns, in order |
| `ended` | The session was ended | Not run |
| `undeliverable` | The question could not be posted to Linear | Run as ordinary turns, in order |
Interrupting a turn does not close a question. In Linear, messages typed
while the agent works usually arrive after the answer; see
[Ask before acting](/agents/linear#ask-before-acting).
## Durability and recovery
Closing the browser or redeploying your application does not discard work
the API has accepted. Reopen the session and resume reading its durable
event log. If a submission's response was lost, retry with the same request
key and body; see [Starting work from an application](/agents/api#start-work-from-an-application).
Durable history does not make every command resumable. A lost computer can
leave a command's outcome unknown; OpenComputer does not automatically
replay that command or roll back its effects. The turn can continue with a
tool error or fail. Inspect the recorded outcome and verify external
effects before asking the agent to retry.
[Interrupt](/agents/api#end-and-interrupt) stops the current turn while
keeping the session available for more work. End closes the session and
revokes its memory writes. Neither operation undoes completed writes;
their different acknowledgement and cleanup guarantees are described in
the API reference.
## CLI
Start a session against the development agent:
```bash theme={null}
npm run session
```
The session command always uses the current project's bound Development
deployment. It does not select between local and remote runtimes.
Projects with multiple agents may select another member of the bound project.
The environment remains Development:
```bash theme={null}
npm run session -- --agent reviewer "Review this change"
```
Add `--verbose` to stream non-message lifecycle, runtime, tool, egress, usage,
and turn events. Assistant message text continues to stream normally:
```bash theme={null}
npm run session -- --verbose --agent reviewer "Review this change"
```
Or manage the durable session lifecycle explicitly:
```bash theme={null}
npm run session -- create "Draft a response"
npm run session -- list
npm run session -- inspect
npm run session -- attach
npm run session -- send "Continue"
npm run session -- end
```
`list` shows the newest sessions first, one page at a time; narrow it with
exact filters and continue from the cursor the previous page printed. With
`--json` the page is printed as `{ sessions, nextCursor }` for scripts:
```bash theme={null}
opencomputer session list --status suspended --agent reviewer --limit 20
opencomputer session list --external-reference order-42 --json
opencomputer session list --limit 2 --cursor --json
```
`--agent` takes the agent's id, `--limit` is 1 to 100, and `--cursor`
continues only with the filters that produced it.
Bind [memory](/agents/memory) documents when creating a session; add
`--create-document` to create the ones that do not exist yet:
```bash theme={null}
npm run session -- create --memory notes=workshop --create-document "Plan the workshop"
```
Follow the durable event log directly. With `--json`, each event is one NDJSON
record suitable for a coding agent or log processor:
```bash theme={null}
opencomputer sessions tail --after 0 --json
opencomputer sessions tail --after 42 --no-follow --json
```
Project scripts resolve the `opencomputer` binary from the installed
`@opencomputer/cli` package. For a one-off invocation outside those scripts,
select the scoped package explicitly:
```bash theme={null}
npx --package @opencomputer/cli opencomputer session "Draft a response"
```
Use `--keep` on supported commands when the session should remain active after
the command exits.
### Workspace files
Files an agent writes under `/workspace` (reports, captures, screenshots, tool
output) can be listed and downloaded with `session files`. Newly written files
appear gradually and may take a few moments to finish syncing. Refresh or list
the workspace again if a file has just been created.
Downloads never pass through the model or the session transcript: OpenComputer
authorizes the exact versioned workspace object for a short-lived direct
download through its file-delivery edge. The CLI streams that object to disk,
checks its byte count, and computes SHA-256 locally before the file is saved.
```bash theme={null}
# List the files in the session's /workspace
opencomputer session files
opencomputer session files ls
# Download one file (dest may be a file path or an existing directory)
opencomputer session files download report/summary.md
opencomputer session files download /workspace/captures/run.har ./evidence/
# Download the whole workspace, mirroring its layout
opencomputer session files download --all ./workspace-copy
# Machine-readable output with size and locally computed sha256 per file
opencomputer session files download --all ./workspace-copy --json
```
`cp` is an alias for `download`. Workspace paths may be written relative to
`/workspace` or with the `/workspace/` prefix; paths that leave the workspace
are refused before any request is made. Each download is written to a
temporary file and renamed into place only after the expected byte count is
received; SHA-256 is reported from the completed local file. An interrupted
transfer never leaves a partial file behind. The versioned S3-backed workspace
remains the source of the file in the
CLI, the session page's **Files** tab, and the
[playground debug inspector](/agents/playground).
## From an application
Applications create sessions with the [management API](/agents/api): `POST
/api/managed-agents/sessions` with an API key, then turns and the event log
under that session. The
[TypeScript client](/reference/typescript-sdk/agents) wraps those
routes; its `sessions.startOnDocument` creates a memory document and a
session bound to it in one call. In the browser, the
[`useAgent`](/agents/react) hook attaches to a session your server created
and renders its messages.
## Input sources
Inside an agent, `useInput().source` identifies how the current work arrived.
This lets the same agent distinguish direct work from delegated subagent work
without defining another session type.
# Skills
Source: https://docs.opencomputer.dev/agents/skills
Package reusable instructions and supporting resources with an agent
A skill is a reusable instruction bundle that teaches an agent how to perform
a task. A tool executes code; a skill provides a procedure, checklist, or
domain playbook the agent can load when relevant.
## Create a skill
Add a directory containing `SKILL.md` under the agent:
```text theme={null}
opencomputer/agents/campaign-writer/
├── agent.ts
└── skills/
└── campaign-brief/
├── SKILL.md
└── CHECKLIST.md
```
Write a clear name and routing description in the frontmatter, then put the
full procedure in the body:
```md theme={null}
---
name: campaign-brief
description: Create a sourced campaign brief. Use when planning a new campaign or revising its audience and messaging.
---
# Campaign brief
1. Restate the product, audience, goal, and constraints.
2. Research the audience before proposing messaging.
3. Separate sourced facts from recommendations.
4. Produce a positioning statement, three message pillars, and five concepts.
5. Read `CHECKLIST.md` before returning the final brief.
```
The description should say both what the skill does and when it applies. The
agent uses it to decide whether to load the instructions.
## Use the skill
Skills do not need a hook. OpenComputer packages the entire `skills/` directory
with that agent, which can load a matching skill during the session.
Tell the agent when the skill should guide its work:
```tsx theme={null}
export default function Agent() {
return [
"Plan and improve marketing campaigns.",
"For a new campaign brief, use the campaign-brief skill and follow its checklist.",
].join(" ");
}
```
## Add supporting files
Keep the core procedure in `SKILL.md`. Put longer material beside it:
```text theme={null}
campaign-brief/
├── SKILL.md
├── CHECKLIST.md
├── VOICE.md
└── examples/
└── launch-brief.md
```
Reference those files from `SKILL.md` so the agent knows when to read them.
Supporting files are deployed with the skill and need no imports from
`agent.ts`. Loading a skill lists them, and the agent reads one on request —
so a long checklist or a set of examples costs nothing until it is needed.
An agent can only read files packaged with the skill it loaded. There is no
path out of that directory, and no way to reach anything else on the machine.
## Scope skills to an agent
Skills live under `opencomputer/agents//skills/`, so each deployment
receives only its own skill set. To reuse a skill, copy or generate the skill
directory into each agent that needs it.
## Skills versus tools
| Skill | Tool |
| - | - |
| Markdown instructions and resources | Executable TypeScript |
| Teaches a repeatable workflow | Performs a specific operation |
| Loaded when its description matches | Called with JSON Schema arguments |
| May tell the agent which tools to use | Produces a JSON-compatible result |
Many useful workflows combine both: a research skill explains the process
while the [GTM engineer example](/agents/examples/gtm-engineer) uses an Exa
search tool to retrieve current sources.
# Slack connections
Source: https://docs.opencomputer.dev/agents/slack
Connect a deployed agent to Slack from the dashboard so people can talk to it in threads
A Slack connection gives a deployed agent its own Slack app. Mention the app or
send it a direct message and OpenComputer starts a session for that thread;
follow-ups in the same thread continue the same session. There is no Slack
client, message parser or reply code to write: the agent reads the message
through [`useInput()`](/agents/inputs) and its output is posted back to the
thread.
Slack is connected per project and per environment from the dashboard.
Development and Production never share Slack credentials or thread routing, so
connecting an app in one environment does not connect it in the other.
## Connect Slack
After the project's deployment is synchronized:
1. Open the project in the dashboard and select **Development** or
**Production**.
2. Open **Connections**. The Slack panel shows the agent that will receive
messages.
3. Choose **Create Slack bot** and enter the bot's name.
4. Paste an **App configuration access token** from
[your Slack apps](https://api.slack.com/apps): under **Your App
Configuration Tokens**, choose **Generate Token**, choose the workspace the
bot will live in, and copy the **Access Token** (not the refresh token).
5. Approve the installation in Slack. You are returned to the same project
and environment.
6. Invite the bot to each conversation it will use and mention it.
OpenComputer creates the app with the standard configuration (mentions and
direct messages) and installs it into your workspace. The configuration token
is used once, to create the app, and is never stored.
The panel distinguishes **Connected**, meaning the installed credentials are
stored, from **First message received**, meaning a real Slack message reached
the agent. Slack's URL verification is not a message; mention the bot once to
confirm delivery.
If Slack's consent page is closed before approval, the created app is kept:
open Connections again and choose **Authorize in Slack**. When the outcome of
app creation is unknown, the panel says so and links to your Slack app list
rather than creating a second app; finish with manual setup if the app exists,
or cancel and start again if it does not. Cancelling a setup never deletes an
app in Slack.
### Set up manually
Manual setup remains available for workspaces where you cannot generate a
configuration token, or to connect an app you created yourself. In the Slack
panel choose **Set up manually**:
1. Create or update the Slack app from the generated manifest.
2. Install the app to the intended Slack workspace.
3. Enter the installed app's ID, signing secret and bot token when prompted.
4. Invite the app to each Slack conversation it will use.
Both paths produce the same app configuration and the same connection.
Credentials and disconnecting are managed in the same place afterwards.
## Questions
When an agent calls [`ask`](/agents/tools#ask-a-question), the thread gets one
message: the turn's text, then the question with one button per option.
Clicking a button, or replying in the thread after the question, answers it.
Messages sent before the question was posted reach the agent with the answer,
as steering. Only the person the session is talking to can answer; other
people's clicks are ignored. Once the question is answered or closed, the
buttons are replaced by the choice or by "closed (reason)".
## Current limits
* A Slack app is installed into a single workspace; organization-wide
installations are not supported.
* Direct Slack user mapping and outbound DMs are not available.
* Proactive messages to a Slack conversation that did not start the session
are not part of the connection. Persist pending notifications in the
project's [database](/agents/database) and deliver them from a
[scheduled](/agents/schedules) worker instead.
See [A coding agent in Slack](/agents/examples/slack-coder) for a complete
deployed example.
# Tools
Source: https://docs.opencomputer.dev/agents/tools
Define TypeScript tools and expose them with useTool
A tool is TypeScript code the model may call while it works. Use tools for
actions such as searching an API, looking up an order, or updating an external
system. The model decides when a tool fits; your `run` function controls what
happens.
## Define your first tool
Create a tool beside the agent, for example
`opencomputer/agents/support/tools/lookup-order.ts`:
```tsx theme={null}
import { defineTool } from "@opencomputer/agent";
export const lookupOrder = defineTool({
name: "lookup_order",
description: "Look up an order by ID and return its current status",
input: {
type: "object",
properties: {
orderId: { type: "string", description: "The customer order ID" },
},
required: ["orderId"],
additionalProperties: false,
},
async run({ input }) {
const orderId = String(input.orderId);
return { orderId, status: "processing" };
},
});
```
Then attach it from `agent.ts`:
```tsx theme={null}
import { useModel, useTool } from "@opencomputer/agent";
import { lookupOrder } from "./tools/lookup-order";
export default function Agent() {
useModel("anthropic/claude-sonnet-4.6");
useTool(lookupOrder);
return "Help customers check orders. Ask for an order ID before lookup.";
}
```
The model sees the tool's name, description, and input schema. When it calls
the tool, OpenComputer runs `run()` in the managed agent runtime and returns
the result to the model.
## Tool definition
| Field | Required | Purpose |
| - | - | - |
| `name` | yes | ID the model calls; letters, numbers, underscores, and hyphens only |
| `description` | yes | Explains what the tool does and when to use it |
| `input` | no | JSON Schema for tool arguments |
| `output` | no | JSON Schema describing the result; required on the result tool |
| `result` | no | `true` marks this tool as the agent's [result tool](#the-session-result); one per agent, and it must have `run` |
| `run` | yes\* | Synchronous or asynchronous implementation |
| `preview` / `apply` | no | Supply these *instead of* `run` to [wait for a person](#wait-for-a-person-before-writing) |
\* A tool has either `run`, or `preview` and `apply` — never both, and never
just one of the pair.
Descriptions are the model's documentation. Include the action, when it is
appropriate, and any important precondition.
## Execution context
The `run` function receives:
| Value | Purpose |
| - | - |
| `input` | Arguments created for the tool call |
| `sessionId` | Current session ID |
| `messageId` | Message containing the tool call |
| `toolCallId` | ID of this invocation, unique within the session |
| `agentId` | Agent executing the call |
| `signal` | Optional cancellation signal for abortable work |
| `reportProgress(metadata)` | Emits JSON-compatible progress metadata |
Use the cancellation signal with `fetch()` and report progress before slow
steps:
```tsx theme={null}
export const importCatalog = defineTool({
name: "import_catalog",
description: "Import the latest product catalog; use only when asked",
async run({ signal, reportProgress }) {
await reportProgress({ phase: "downloading" });
const response = await fetch("https://example.com/catalog.json", {
signal,
});
await reportProgress({ phase: "processing" });
return { imported: response.ok };
},
});
```
Tool results and progress metadata must be JSON-compatible values.
For an external write, `${sessionId}:${toolCallId}` can identify the invocation
in a downstream idempotency key. It stays the same when that invocation is
retried. A new call from the model has a new ID, even if it requests the same
action; use a business-level key when those calls must also converge.
## The session result
An agent may mark one tool as its result tool. Its latest committed output
is the session's `result`: the structured outcome an application reads
instead of parsing the final message. This tool checks a URL itself rather
than accepting a claimed status from the model:
```tsx theme={null}
import { defineTool } from "@opencomputer/agent";
const checkResult = {
type: "object",
properties: {
url: { type: "string" },
status: { type: "integer" },
},
required: ["url", "status"],
additionalProperties: false,
} as const;
export const checkUrl = defineTool({
name: "check_url",
description: "Check an HTTPS URL and report its HTTP status",
input: {
type: "object",
properties: { url: { type: "string" } },
required: ["url"],
additionalProperties: false,
},
output: checkResult,
result: true,
async run({ input, signal }) {
const url = new URL(String(input.url));
if (url.protocol !== "https:") throw new Error("Use an HTTPS URL");
const response = await fetch(url, { method: "HEAD", signal });
return { url: response.url, status: response.status };
},
});
```
The build enforces three rules: an agent declares at most one result tool,
a result tool declares `output`, and it runs with `run()` rather than
[waiting for a person](#wait-for-a-person-before-writing). The schema is
recorded with the deployment, and every value the tool returns is validated
against it.
A call to the result tool succeeds only once its output is committed to
the session:
* The value `run()` returns is validated against `output` and committed
before the model is told the call succeeded. The session's `result`
becomes `{ turnId, callId, reportedAt, data }`, and the call's
`tool.completed` event carries `result: true`
([Session events](/agents/events#tools)).
* A value that does not match `output`, or exceeds 8 KiB of JSON, fails the
call; the model sees the reason and can call again. The previous result
stands.
* A call from a turn that was interrupted or has ended, or from a runtime
the session no longer runs on, is refused the same way.
* A later call replaces the entire value. Reporting does not finish the turn.
A subsequent turn that fails or is cancelled does not clear the result, and
the event log keeps every call.
The result says what the agent reported, not that the work succeeded.
Verify claims inside `run()`, where a tool can check that a branch exists
or a pull request is open, and throw to reject them; let the application
check what the result references. Read it from
[`GET /sessions/`](/agents/api#get-and-list). For a review-ready UI, also
check current activity and whether `result.turnId` belongs to the last settled
turn. An existing result may describe earlier work.
If the commit acknowledgement is lost, the tool can fail even though its
result was saved. OpenComputer retries the commit for the same tool call;
that deduplicates the saved result, not external writes inside `run()`. Prefer
tools that verify and report existing work. Give any external write its own
idempotency key.
### Schema declarations
The result tool's `output` is copied into the deployment without evaluating
code. Use one of these forms:
* An object literal in the `defineTool()` call.
* A `const` object literal in the same module, as above.
* A named import of such a `const` from a module inside the agent directory.
Function calls, `let` bindings, namespace members such as `schemas.report`,
package imports and re-exports are not supported for this `output` schema.
The build error names the unsupported form. `input` may use these static
forms or a schema constructed when the tool module loads; it is not subject
to result-output extraction. Neither schema infers the TypeScript type of
`run`'s `input`, which remains `Record`. `as const` preserves
the schema's literal types for your own typing and validation.
Write `name`, `result` and `output` as ordinary properties on the
`defineTool()` object. Spreads, computed property names, and a `result` value
other than literal `true` or `false` fail the build. A tool module whose
`result` declaration disagrees with its deployment fails to load.
## Conditional tools
`useTool()` may be conditional. This keeps sensitive or specialized actions
out of the tool set until they are relevant:
```tsx theme={null}
import { useInput, useTool } from "@opencomputer/agent";
import { issueRefund } from "./tools/issue-refund";
export default function Agent() {
const input = useInput();
if (input.text?.toLowerCase().includes("refund")) {
useTool(issueRefund);
}
return "Resolve the support request. Confirm the order before a refund.";
}
```
`useTool()` also accepts a tool ID such as `useTool("web-search")` when the
runtime already provides that tool.
## Give an agent a sandbox shell
Select the built-in `sandbox_exec` tool when an agent needs to run commands,
inspect files, or use installed CLI packages:
```tsx theme={null}
import { useTool } from "@opencomputer/agent";
export default function Agent() {
useTool("sandbox_exec");
return "Use the sandbox for shell commands and keep durable work in /workspace.";
}
```
The agent runtime starts without a VM. The first `sandbox_exec` call lazily
acquires an isolated computer for the session, and later calls in that session
reuse it. Files that must survive restarts belong in `/workspace`; temporary
paths such as `/tmp` do not provide that persistence guarantee.
If your organization has an Enterprise custom package image, `sandbox_exec`
uses that image automatically. The agent definition still selects the same
tool ID, and cannot choose or override the image itself. You can see the
package image assigned to your organization under **Settings → Customize VM
packages**.
## Call external APIs safely
For authenticated HTTP APIs, declare a connection, store the key as an
OpenComputer secret, and call the connection's `fetch()` method. This keeps
the credential outside the agent runtime; see
[Secrets and outbound requests](/agents/secrets).
[Managed GitHub connections](/agents/github) are the exception: they supply
short-lived environment credentials so Git and `gh` work directly. Never
embed credentials in tool source or return them to the model.
For Google services, GitHub, or Linear, the credential belongs to whoever
connected the account and the platform refreshes it — call those with
[`callService`](/agents/services) instead.
## Ask a question
`ask` is the platform's question tool. Select it with `useTool("ask")` when a
turn may need to stop and ask a person before going on:
```tsx theme={null}
import { useInput, useTool } from "@opencomputer/agent";
export default function Agent() {
const input = useInput();
if (input.answer) {
return `The answer was ${input.answer.value ?? input.answer.text}. Proceed accordingly.`;
}
useTool("ask");
return "Read the request, propose an approach, and ask whether to go ahead.";
}
```
The model calls it with:
| Field | Bounds |
| - | - |
| `text` | The question, 1 to 4000 characters |
| `options` | Optional, at most six `{ label, value }` choices; each `label` and `value` 1 to 80 characters |
Calling `ask` ends the turn with an open question. The turn completes with
`outcome: "question"`, the session's `question` holds what was asked, and
`question.asked` is recorded in the [event log](/agents/events#questions).
The reply arrives as the next input with
[`answer`](/agents/inputs#answers-and-steering) set, and anything written
meanwhile arrives with it as `steering`, within
[bounds](/agents/sessions#questions). A session has at most one open
question; the turn that interprets the answer may ask again.
How the question reaches a person depends on where the session lives: an
application answers it through the [API](/agents/api#turns) or the
[React hook](/agents/react#answer-a-question); a
[Slack](/agents/slack#questions) or [Linear](/agents/linear#ask-before-acting)
session shows it in the thread with the options as buttons, and a click or a
typed reply answers it.
`ask` records that the agent asked. It does not undo or forbid anything the
turn did before asking. A turn meant only to propose should select `ask` and
tools that cannot write, and select the writing tools on the turn that
carries the answer.
The id `ask` is reserved: `defineTool({ name: "ask" })` is refused, both by
`defineTool` and by the CLI's build. `ask` runs on the default runtime for new deployments;
sessions with `executionMode: "microvm"` cannot select it.
`ask` is different from [waiting for a person](#wait-for-a-person-before-writing):
`ask` ends a turn with an open question the agent reasons about when it is
answered, while an approval tool records one exact write that runs, unchanged,
once someone approves it.
## Wait for a person before writing
Some writes should not happen because a model decided they should. Such a tool
proposes instead: the model calls it, nothing is written, and the person the
agent is talking to sees a card with the exact change and decides.
There is no separate primitive for this. Give `defineTool` a `preview` and an
`apply` instead of a `run`, and the model's call becomes a proposal. `run` is
written for you. `useTool` selects the tool exactly as it selects any other,
and the model cannot tell the difference — waiting for approval is a property
of a tool, not a different kind of thing.
```tsx theme={null}
import { defineTool } from "@opencomputer/agent";
import { billing } from "./connections/billing";
export const attach = defineTool({
name: "attach",
description: "Move a customer onto a plan",
input: {
type: "object",
required: ["customerId", "planId"],
properties: {
customerId: { type: "string" },
planId: { type: "string" },
},
},
// What the person sees. It may read; it must not write.
async preview({ input, signal }) {
const customer = await billing
.fetch(`/customers/${String(input.customerId)}`, { signal })
.then((response) => response.json());
return {
title: `Move ${customer.name} to ${String(input.planId)}`,
facts: [
{ label: "Today", value: customer.plan },
{ label: "After", value: String(input.planId) },
{ label: "Charged now", value: customer.nextCharge },
],
};
},
// Runs only once somebody approves, from the arguments on the card.
async apply({ input, decision, signal }) {
return billing.fetch("/subscriptions", {
method: "POST",
headers: { "Idempotency-Key": decision.id },
body: JSON.stringify({
customer: input.customerId,
plan: input.planId,
}),
signal,
});
},
});
```
The model calls `attach` like any other tool and is told the change was
recorded for approval, so it stops rather than reporting the work as done.
`apply` runs later, with the arguments that were on the card — no second pass
through the model, so what happens is what was agreed to. The proposing session
is usually gone by then, and that is fine: `apply` needs its arguments, not the
conversation.
### Always pass `decision.id`
`decision.id` identifies this approval, and it is the same value on every
attempt to carry it out. Give it to whatever you call as an idempotency key.
If a write never reports back — the runtime died mid-flight, the answer was
lost — it is recorded as unconfirmed rather than failed, because it may well
have happened. That is only recoverable if running it again is safe, and this
is what makes it safe.
### What this does and does not guarantee
Approval is a convention your code cooperates with. Nothing stops a tool from
writing inside `preview`. What the platform guarantees is narrower and
still worth having: an approved write runs **once**, from the arguments the
person agreed to, whether or not the session that proposed it still exists.
A tool that waits needs a conversation to ask in. Called from the playground,
a schedule or a webhook, there is nobody to ask, and the call fails saying so.
## Computer commands
A session gets an isolated computer when it needs one; conversation alone
does not require a computer. The model runs commands through the built-in
`shell` tool. A command runs in the session's workspace and takes `command`
and an optional `timeoutSeconds`: 120 by default, at
most 1800, and never more than the computer's remaining lifetime allows; a
value out of range fails the call before anything runs. A command is not
started on a computer that cannot host it for its full timeout; a fresh
computer is used instead.
When the timeout passes, the command and every process it started are
stopped, and the call fails as a tool error the model sees: it names the
command and the timeout and carries the output so far. The turn continues,
and files the command wrote before it was stopped remain.
A command counts as stopped only once that is confirmed: every process it
started, detached ones included, is gone, or the computer itself was
terminated. When the computer stops answering while a command runs, its
outcome is unknown, and the call fails with that as the reason; the model
is never told a command was stopped when that was not confirmed. That
computer runs no further command until the unknown one is confirmed
stopped or the computer is replaced by a fresh one with the session's
workspace, which the next command does on its own. See
[End and interrupt](/agents/api#end-and-interrupt) for cancellation guarantees
and the compatibility limit for sessions using the older runtime.
## Tools versus skills and MCP
* A tool executes TypeScript you own.
* The same tool with `preview` and `apply` instead of `run`
[waits for a person](#wait-for-a-person-before-writing) before it does it.
* A [skill](/agents/skills) teaches the agent a reusable procedure.
* An [MCP server](/agents/mcp) supplies a remote collection of tools.
# Agent webhooks
Source: https://docs.opencomputer.dev/agents/webhooks
Start an agent session from an external service
Agent webhooks are stable, authenticated ingress points for an agent. Each
webhook targets one project agent and one environment. Calling it starts a
fresh durable session against the deployment active in that environment.
Webhooks are operational configuration, so create them in the dashboard or
CLI rather than in agent source. Advancing a deployment does not change the
webhook URL.
## Create a webhook
Open an agent's **Webhooks** tab, select Development or Production, and choose
**Create webhook**. The dashboard shows the webhook URL once. The URL carries
the credential: store it in the calling service's secret store, and treat it
as you would a token.
The CLI supports the same lifecycle:
```bash theme={null}
opencomputer webhooks create daily-hygiene \
--agent current \
--environment production
opencomputer webhooks list --agent current --environment production
opencomputer webhooks disable
opencomputer webhooks enable
opencomputer webhooks rotate-token
opencomputer webhooks remove
```
`create` and `rotate-token` print the URL with its token once. Rotation
invalidates the previous URL immediately. `list` prints URLs without tokens.
## Invoke it
Send `POST` with `application/json` to the URL:
```bash theme={null}
curl -X POST 'https://app.opencomputer.dev/api/agent-webhooks/wh_.../ocwh_...' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: delivery-123' \
-d '{
"text": "Run the feature-flag hygiene review.",
"payload": {
"mode": "hygiene",
"repository": "acme/widgets"
}
}'
```
`text` becomes the initial turn prompt. `payload` remains structured JSON
available to the agent. A body with neither key is delivered whole as
`payload`, so a provider such as Sentry, GitHub, or Stripe can point its
webhook at the URL directly; the prompt is then `Webhook invoked`.
Bodies are limited to 256 KiB.
A sender that sets headers may omit the last path segment and send the token
as `Authorization: Bearer ` instead. Both forms verify the same
credential.
The response is HTTP 202 with the request, session ID, and dashboard session
URL. It is sent as soon as the delivery is stored; the session starts
afterwards, so acknowledgement does not wait for a runtime.
The request's `outcome` is `pending` in the response. It becomes `accepted`
once the session holds the turn, which is the point of no return: from
there the session runs it, even if the runtime has to be started again.
It becomes `failed` after five start attempts over about thirteen minutes,
or at once when the platform can tell that no retry would help; such a
request carries `terminal: true`. Read outcomes from the request ledger
with your API key; the session id there is authoritative, since a retried
start can use a different session than the one first announced:
```bash theme={null}
curl 'https://app.opencomputer.dev/api/managed-agents/projects//webhooks//requests' \
-H 'x-api-key: '
```
Each delivery needs an identity so that a retry does not start a second
session. A sender that can set headers sends `Idempotency-Key`. For a
provider that cannot, give the webhook an identity source when you create
it: a request header, or a JSON Pointer into the body.
```bash theme={null}
opencomputer webhooks create sentry-alerts --agent current --environment production \
--identity body:/data/event/event_id
opencomputer webhooks update --identity header:X-GitHub-Delivery
opencomputer webhooks update --identity none
```
Choose a value that stays the same when the provider retries and differs
between deliveries you want handled separately. Sentry's per-request
`Request-ID` changes on every retry, so for its issue alerts use the event
id in the body. Without a source, every delivery that sets no
`Idempotency-Key` starts a session.
Retrying with the same `Idempotency-Key` and a different body is a 409. A
retry under a configured source returns the original request even if the
provider reserialized the body. If the original request had failed to
start after its attempts, the retry starts it again; if it failed with
`terminal: true`, the retry returns that failure unchanged. To recover a
terminal failure, resend the saved payload with a fresh `Idempotency-Key`,
which overrides the configured source and starts a new request. A body
larger than 256 KiB once wrapped as agent input is rejected with 400
before acknowledgement.
A header source cannot name a header the platform sets or strips before
the backend reads it: `Authorization`, `Cookie`, `Idempotency-Key`,
`X-Request-Id`, `X-Api-Key`, transport headers, and anything starting with
`x-oc-`, `cf-`, `x-forwarded-`, or `x-real-`. Creating a webhook with one
of these is rejected.
## Read webhook input
Use `useInput()` as with other session sources:
```tsx theme={null}
import { useInput } from "@opencomputer/agent";
export default function Agent() {
const input = useInput();
const payload =
input.payload &&
typeof input.payload === "object" &&
!Array.isArray(input.payload)
? input.payload
: {};
if (payload.mode === "hygiene") {
return `Run the configured hygiene workflow for ${payload.repository}.`;
}
return input.text ?? "Ask the caller what workflow to run.";
}
```
Use payload fields such as `mode` for business behavior. `input.source` is
`"webhook"` and `input.webhook` contains the webhook ID, request ID, and receive
time for provenance and correlation.
## Provider deliveries
A provider body arrives unchanged as `input.payload`. Narrow it to what the
agent needs; for a Sentry issue alert that is the event under `data.event`:
```tsx theme={null}
const payload = (input.payload ?? {}) as {
data?: { event?: { event_id?: string; release?: string } };
};
const event = payload.data?.event;
```
Configure the provider with the full webhook URL and set the webhook's
identity source to the field that names the delivery, for example
`body:/data/event/event_id` for a Sentry issue alert.
Development and Production webhooks have separate URLs, tokens, and sessions.
Disable or remove a webhook when its caller should no longer start sessions.
The webhook routes are listed on the [management API](/agents/api#webhooks)
page.
# Serverless Agents client
Source: https://docs.opencomputer.dev/reference/typescript-sdk/agents
Create sessions, submit work and read results from your application's server
`@opencomputer/sdk/agents` is the TypeScript client for the
[management API](/agents/api): create sessions, send turns, read the event
log, list and label sessions, and manage a project's memory, webhooks, event
subscriptions and GitHub repositories. Use it in your application's server;
use [`@opencomputer/react`](/agents/react) in the browser and
[`@opencomputer/agent`](/agents/hooks) to define agent behavior.
The client uses web-standard APIs and runs on Cloudflare Workers without
Node compatibility, Vercel functions, Deno and Node. The package root,
`@opencomputer/sdk`, provides the separate sandbox client.
```bash theme={null}
npm install @opencomputer/sdk
```
```typescript theme={null}
import { OpenComputer, OpenComputerError } from "@opencomputer/sdk/agents";
const oc = new OpenComputer({ apiKey: process.env.OPENCOMPUTER_API_KEY! });
```
Use it from trusted server code. The key belongs to an organization and
reaches every project, agent and session in it; it must not reach a browser.
Authenticate your users and check their access before forwarding requests.
> [!NOTE]
>
> This client requires SDK 2.0 or later. Version 2 removes the older session
> client and moves `startSessionOnDocument`, memory types and event-subscription
> types to `@opencomputer/sdk/agents`. See the
> [migration notes](https://github.com/diggerhq/opencomputer/blob/main/sdks/typescript/CHANGELOG.md#200)
> for changed imports and errors. Applications using the older session API
> should remain on SDK 1.1.1 until migrated.
## `new OpenComputer(options)`
| Option | Default | Description |
| - | - | - |
| `apiKey` | required | An OpenComputer API key |
| `baseUrl` | `https://app.opencomputer.dev/api/managed-agents` | The management API |
| `fetch` | the global `fetch` | A fetch implementation, for runtimes without a global or for tests |
Methods accept an `AbortSignal` in their options; `startOnDocument` takes
`signal` in its params. Return shapes are listed below. Paged session lists
return `{ sessions, nextCursor }`; repository lists return
`{ repositories, nextCursor }`. Event reads return an array.
## Errors
HTTP failures throw `OpenComputerError` with `code`, `status` and
`message`. `code` is the API's stable code when the body carried one
(`idempotency_conflict`, `session_ended`, `insufficient_credits`,
`session_publication_unconfirmed`); when the body had only text, it is
derived from the status: `unauthorized`, `forbidden`, `not_found`,
`conflict`, `rate_limited`, `unavailable` for `5xx`, `request_failed`
otherwise. A `429` also carries `retryAfter` in seconds, and an error the
API tied to a session carries `sessionId`.
Network failures, response-body read failures and cancellation propagate the
underlying error, such as `TypeError` or `AbortError`. They do not prove that
a write was rejected. A request can commit before its response is lost.
The client does not retry automatically. Keep the same parameters and key
for a submission's retries; use a new key only for new work. Use bounded
backoff and an `AbortSignal`, and retain the submission if the retry budget
expires. Aborting the HTTP request does not cancel admitted work.
| Failure | How to recover |
| - | - |
| Lost response or `invalid_response` after a write | Treat the outcome as unconfirmed. Retry create/send with the same key and input; do not create a replacement submission. |
| `503 session_publication_unconfirmed` | The session or labels were recorded, but list visibility was not confirmed. Repeat create with its original key and params, or repeat the same label patch. |
| `503 memory_admission_unconfirmed` | Retry the same admission; see [memory admission](/agents/api#create). |
| `503 session_end_unconfirmed` | Retry `sessions.end(id)` to confirm memory-write revocation. |
| `409 idempotency_conflict` | The key names different inputs. Recover the original submission; do not silently replace its key. |
A successful create or label update confirms publication to the list. See
the [Management API](/agents/api) for each operation's errors and guarantees.
The client does not follow redirects. The API key is sent to `baseUrl` and
to no other origin: a redirect answer fails the call with code `redirected`
and the redirect's status, and nothing is sent to the address it named.
Check `baseUrl` when you see it.
Two codes come from the client rather than the API:
| `code` | Meaning |
| - | - |
| `redirected` | The call was answered with a redirect; the client never follows one with the API key. `status` is the redirect's status. |
| `invalid_response` | The HTTP response succeeded but its body is not JSON or fails the client's shape checks. `status` is the HTTP status; the message names the route and, for shape failures, the field. |
## Validation
The client checks response envelopes and selected fields before returning;
unknown fields pass through. Application-owned values such as
`session.result.data` and turn payloads still need your application schema.
Event reads check `seq`, `type` and that `data` is an object. They do not
validate each known event type's payload; identity and timestamp fields are
checked only when present. The exported event types describe the API contract,
not complete runtime validation. Validate event data before using it where
its contents affect application correctness or access.
## Sessions
| Method | Route | Returns |
| - | - | - |
| `sessions.create(params, { idempotencyKey? })` | `POST /sessions` | `{ session, deployment, created }`; `created` is `false` when the key had already created the session |
| `sessions.get(id)` | `GET /sessions/` | The session with its `turns` and `memory` |
| `sessions.list(query?)` | `GET /sessions` | `{ sessions, nextCursor }`: one page |
| `sessions.iterate(query?)` | `GET /sessions`, page by page | An async iterator over every matching row |
| `sessions.end(id)` | `POST /sessions//end` | The ended session |
| `sessions.interrupt(id)` | `POST /sessions//interrupt` | The session |
| `sessions.setLabels(id, { set?, unset? })` | `PATCH /sessions//labels` | The session |
| `sessions.turns.send(id, params)` | `POST /sessions//turns` | `{ turnId, status, duplicate }`, or a held receipt |
| `sessions.questions.dismiss(id, questionId)` | `POST /sessions//questions//dismiss` | Closes the open question with `dismissed` |
| `sessions.events.list(id, { after? })` | `GET /sessions//events` | The events with `seq` greater than `after`, up to 500 |
| `sessions.startOnDocument(params)` | `PUT` the document, then `POST /sessions` | `{ document, session }` with `created` on each |
`create` takes `agentId`, `deploymentId`, `environment`, `memory`, `source`,
`labels` and `externalReference` as [documented](/agents/api#create). `turns.send` takes `input`,
`idempotencyKey`, `mode`, `payload` and `answers`, the id of the open
[question](/agents/sessions#questions) the input answers; both `create` and `turns.send` send
the key as the `Idempotency-Key` header. The receipt carries the turn's
persisted `status`: `queued` or `running` for a new turn, and for a repeated
key the existing turn's status, `completed`, `failed` or `cancelled` once it
has settled, with `duplicate: true`. The client maps nothing, so a retry
after a lost reply learns what became of the turn. An input sent while a
question is open, without `answers`, runs no turn: its receipt is
`{ status: "held", questionId, duplicate }` with no `turnId` (`"discarded"`
once a stop ended the question). `list` takes the exact
filters of [`GET /sessions`](/agents/api#get-and-list): `project` or
`projectId`, `environment`, `agent` or `agentId`, `status`, `deploymentId`,
`externalReference`, `createdAfter`, `createdBefore`, `updatedAfter`,
`labels` (up to three, sent as `label.=`), plus `cursor` and
`limit` (1 to 100, default 50). `iterate` takes the same query and requests
the next page whenever the previous one is consumed, stopping when
`nextCursor` is `null`; it reads live rows, so a session created while you
iterate may or may not be seen.
```typescript theme={null}
const recovered = await oc.sessions.list({ externalReference: orderId });
for await (const row of oc.sessions.iterate({ status: "suspended", limit: 100 })) {
console.log(row.id, row.externalReference);
}
```
### Create and submit
Create the session, then send its first turn. Address the agent by its
environment alias and retain both keys and the inputs. Retrying either call
then returns the same session or turn, even if the agent is redeployed
between attempts: the platform records the deployment the session started
on. Pinning a `deploymentId` is an advanced option, not part of this flow.
```typescript theme={null}
const { session } = await oc.sessions.create(
{ agentId: "@development", labels: { request: taskId } },
{ idempotencyKey: taskId },
);
const receipt = await oc.sessions.turns.send(session.id, {
input: text,
idempotencyKey: `${taskId}/start`,
payload: { repo, ref },
});
```
Send follow-ups to the same session with a fresh key per submission. To
display work in a browser, [attach the React hook](/agents/react) through
your authenticated application routes.
### Read progress and results
Read events from sequence `0`, then request only events after the last
sequence processed:
```typescript theme={null}
const events = await oc.sessions.events.list(session.id, { after: 0 });
const cursor = events.at(-1)?.seq ?? 0;
const next = await oc.sessions.events.list(session.id, { after: cursor });
```
Each read returns up to 500 events. Continue immediately while catching up;
when a read is empty, wait before polling again. A cursor skips earlier
events; it does not reconstruct state you have discarded. See
[Session events](/agents/events) for payloads and replay.
`sessions.get(id)` returns the latest typed result, if the agent has reported
one. Results retain their turn provenance and can outlive later work; see
[Sessions](/agents/sessions) before treating a result as ready for review.
### Start a session on a memory document
`sessions.startOnDocument` creates a [memory document](/agents/document-memory)
if it does not exist, then a session bound to it, and reports both ids and
whether each already existed. These are two requests, not an atomic operation:
the document remains if session creation fails. Retries reuse the document
and converge on the session while the agent, environment and bindings remain
unchanged.
```typescript theme={null}
const { document, session } = await oc.sessions.startOnDocument({
projectId: "",
environment: "development",
agent: "",
resource: "notes",
documentId: "workshop",
document: { title: "Workshop notes" },
idempotencyKey: "topic/workshop/1",
});
```
| Parameter | Type | Default | Description |
| - | - | - | - |
| `projectId` | string | required | Project whose memory holds the document |
| `environment` | `"development"` \| `"production"` | required | Memory store and session environment |
| `agent` | string | required | Agent ID; the environment supplies the alias |
| `resource` | string | required | Declared memory resource ID |
| `documentId` | string | required | Document to bind |
| `document` | `{ title, text?, summary?, agentWrites? }` | required | What to create when the document does not exist; ignored when it does |
| `access` | `"read"` \| `"read-write"` | `"read-write"` | The binding's access |
| `memory` | `MemoryBindings` | none | Further bindings keyed by resource ID |
| `idempotencyKey` | string | required | One key for the whole operation |
| `source` | string | `"api"` | The session's `source` |
| `signal` | AbortSignal | none | Aborts both requests |
The steps, in order:
1. `PUT /projects//memory//documents/` with
`If-None-Match: *` and the `document` body. `201` means the document was
created. `412` means it exists; the call reads it with `GET` and leaves
its content as it is. A `404` on that read means the ID was deleted and is
reserved, which fails with code `memory_document_deleted`, since a binding
to it would fail admission.
2. `POST /sessions` with `agentId: "@"`, `source`, and
`memory` set to your further bindings plus
`{ [resource]: { scope: "document", id: documentId, access } }`. The
`Idempotency-Key` header is `sessionIdempotencyKey(idempotencyKey)`, the
SHA-256 hex digest of `opencomputer.memory.session`, a NUL byte and your
key. `201` created the session; `200` means the key had already created it.
Anything else under that key fails with code `idempotency_key_reused`; the
document was left as it is.
The result:
| Field | Description |
| - | - |
| `document.id`, `document.title` | The document |
| `document.created` | `false` when it already existed |
| `document.revision` | Current revision, for a later conditional owner write |
| `session.id` | The session |
| `session.created` | `false` when the key had already created this session |
| `session.status`, `session.executionMode` | As the create response reported them |
The helper addresses the environment's agent alias on every call; the
platform records the deployment the session started on, so a retry after a
redeploy returns the same session. A new key would create another session;
it does not recover a lost response.
`startSessionOnDocument({ apiKey, baseUrl?, fetch?, ...params })`, exported
from the same subpath, is the standalone form of the call for code that
holds a key and no client, and `sessionIdempotencyKey(key)` derives the
session-create key.
## Projects
| Method | Route | Returns |
| - | - | - |
| `projects.list()` | `GET /projects` | The projects |
| `projects.get(id)` | `GET /projects/
` | `{ project, deployments, sessions, connections, channels, schedules }` |
| `projects.create({ name, slug? })` | `POST /projects` | The project |
### Memory
Every call takes `{ environment }` last; conditional writes also take the
`revision` the write is conditional on, sent as `If-Match`.
| Method | Route |
| - | - |
| `projects.memory.resources(p, { environment })` | `GET /projects/