Start
The fields, response, Idempotency-Key, list, share scope, title, and errors of a session create request
A session is a conversation that runs against one profile. If the profile uses a repository, you pick one repository too. When you create a session, the first message is accepted as turn 1, and the scheduler starts a worker container for that session. The scope is sessions:write. The SDK examples use the werft client made in Install the SDK, and the HTTP API examples use the shell variables API (the server address) and KEY (the key).
In the web console, press "New session with this agent" on the agent's page and send the first message. The session starts on the active release (an agent with GitHub settings starts without a repository, and an older agent with no settings starts on its release's repository), through the same POST /v1/sessions with the signed-in person's authority. When the page can tell ahead of time (no active release, paused, no permission and so on), the button is off and the page says why. A refusal known only after sending, such as a missing credential binding, shows its reason in the dialog.
The GATE-SPEC {…} messages in the examples are scripts for the fake model of the development server. The fake model calls tools and answers exactly as the script says, so the output is always the same. A server started with a real model will not make the same tool calls or print the same output; send it ordinary instructions instead.
Create
const created = await werft.sessions.create({
profile_id: "claude-coding-local",
repository_id: "sample-app",
message: 'GATE-SPEC {"id":"start","steps":[],"final":"started"}',
});
console.log(created);
const sessionId = created.session_id;{
session_id: '…',
turn_id: '1',
receipt_id: '…',
receipt_status: 'accepted',
status: 'queued'
}| Field | Required | Description |
|---|---|---|
profile_id | Yes, unless agent_release_id is set | The profile id (Choose a profile) |
repository_id | Depends on the profile | A repository id in the catalog. If sent, the worker clones this repository. It is not sent when the profile's repository_mode is none, and a required profile needs it, except on a session of an agent with GitHub settings, or for a profile the workspace ceiling decides; either may leave it out and start without a repository. On a session made from an agent release, omitting it starts an agent with GitHub settings without a repository, and it attaches only what it needs, while an older agent with no settings starts on its release's repository (Sessions without a repository) |
message | Yes | The input of the first turn. Up to 32,768 UTF-8 bytes |
model | One of the models the profile allows. If omitted, the default model | |
title | The session's name. If omitted, derived from the first message (Title) | |
agent_release_id | Binds this session to an agent release (Create). Then profile_id, if omitted, is the release's value. If you omit repository_id, an agent with GitHub settings starts without a repository, and an older agent with no settings starts on its release's repository |
The response is 201. The request is only accepted; read the result by fetching the turn (Status and lifecycle).
Idempotency-Key
Every POST in the session API requires an Idempotency-Key header. If you did not receive a response, send it again with the same key and the same body. The server runs the command only once. Session creation and messages return the original response as is, and controls such as interrupt return the current state of that receipt. The same key with a different body is 409 IDEMPOTENCY_CONFLICT.
The SDK makes one key per call and sends that same key on every retry of the call. To carry on with the same command after restarting the process, make the key yourself and pass it as idempotencyKey.
const params = {
profile_id: "claude-coding-local",
repository_id: "sample-app",
message: 'GATE-SPEC {"id":"retry","steps":[],"final":"once"}',
};
const idempotencyKey = crypto.randomUUID();
const first = await werft.sessions.create(params, { idempotencyKey });
const second = await werft.sessions.create(params, { idempotencyKey });
console.log(first.session_id === second.session_id ? "same session" : "two sessions");same sessionUse a new key for a new intent. If you send with a new key after a timeout, you may end up with two sessions.
List
const page = await werft.sessions.list({ limit: 5 });
for (const session of page.items) {
console.log(session.id, session.title, session.status);
}
// for await walks every page.
for await (const session of werft.sessions.list({ status: "idle" })) {
console.log(session.id);
}await gives the first page (items, next_cursor, nextPage()); for await gives the items of every page.
Filter with limit and status. Only the sessions the key can see are returned.
relation narrows those further and never widens them. started is the sessions you started (for a key, the ones the key or its creator started), shared the ones someone else started and shared with you by name or through a team you belong to, and administered the sessions of agents you administer, directly or through a team. Someone else's session that the whole workspace sees appears only under all, the default. q finds sessions whose title contains it, ignoring case, or whose id starts with it. agent_id keeps the sessions created with one of that agent's releases, whichever release.
Each item's agent (id, name, release_version_no, release_label) is the agent and release the session is pinned to. release_version_no is the card version that release runs, and release_label is the release's name (when it was created, such as 2026-10-30_14:30:28). It is null for a session created without an agent release. created_by (user_id, display_name) is who started the session; for a key's session, the person who created the key. It stays the same after a takeover: the person who took the session over can read it, but it appears under started only for the person who started it. channel is where the creating request came from (web, api or slack). An older session whose starter or channel the record cannot tell reads null. slack_permalink is the link to the first message of a session started from Slack (Original Slack message link). It is null for a session created another way or before links were kept.
The SDK has no per-status counts, so read them with the HTTP API. GET /v1/sessions/counts returns how many sessions the list would hold per status, read to its last page with the same relation and q, and their total. A session you cannot see is never counted.
curl -sS "$API/v1/sessions/counts?relation=started" -H "Authorization: Bearer $KEY" | jq -cReference: GET /v1/sessions/counts
repository in the list and the detail is the repository the session was created against. id is the catalog key (the same as repository_id); full_name is a name to show, the repository URL's path without host, credentials, query or .git (owner/name, or group/sub/name on a forge with groups), or the catalog key when the URL does not read that way. branch is the branch the worker cloned, not agent/<session-id> that a push writes to. All three are recorded when the session is created, so they stay the same when an operator later changes the catalog or removes the repository. A session created without a repository has repository: null; a session created before the catalog existed has id: null. The repository URL itself is never in a response.
const session = await werft.sessions.get(sessionId);
console.log(session.repository);{ id: 'sample-app', full_name: 'agent/sample-app', branch: 'main' }Share scope
Read the share scope to see who can see a session. Anyone who can see the session can read it. The scope is sessions:read.
const share = await werft.sessions.share(sessionId);
console.log(share.scope, share.revision);workspace 0scope is one of private (only the creator), people (the people in user_ids), team (the current members of the teams in team_ids), or workspace (every member). A session that was never changed is workspace with revision 0. Depending on a workspace setting, a session of an agent may be seen only by those inside the agent even at workspace, and then audience_limited is true (What members outside an agent can do). The console's session screen shows the scope read-only under Shared with in the Operations panel, and with audience_limited it reads Workspace (people outside the responsible team cannot see it, by workspace setting). POST /v1/sessions/{id}/share, which changes the scope, is called only with a web login, so the SDK has no method for it (setSessionShare).
Title
A title you send is stored with runs of whitespace and line breaks collapsed to one space and the ends trimmed. If that leaves nothing or more than 80 characters (an emoji or a combined character counts as one), the answer is 400. Without one, the title is derived from the first message by the same rule, cut to 79 characters and … past 80. No model is called, so the same message always gives the same title. A message of only whitespace, and a session created before titles existed, read title: null.
The SDK has no rename, so call the HTTP API.
curl -sS -X PATCH "$API/v1/sessions/$SID" -H "Authorization: Bearer $KEY" \
-H 'Content-Type: application/json' -d '{"title":"결제 테스트 고치기"}' | jq '{id, title}'The session's creator (for a key's session, that key), its agent's admins, and owners and admins rename it. Someone who only sees it through its share gets 403; someone who cannot see it, 404. No Idempotency-Key is needed. A change arrives on the SSE stream as a status event carrying title. Reference: PATCH /v1/sessions/{id}
Errors
Server errors arrive in the SDK as APIError. Tell them apart with isApiError(error, code); an error whose retryable is true is resent by the SDK on its own (Retries and Idempotency-Key).
import { isApiError } from "@werft-ai/sdk";
try {
await werft.sessions.create({
profile_id: "claude-coding-local",
repository_id: "sample-app",
model: "claude-opus-9",
message: "hello",
});
} catch (error) {
if (isApiError(error, "UNSUPPORTED_CAPABILITY")) {
console.log(error.status, error.code, error.retryable, error.message);
} else throw error;
}422 UNSUPPORTED_CAPABILITY false Model claude-opus-9 is not on the allowed list of profile claude-coding-local; name a model the profile offers| Response | Meaning |
|---|---|
| 400 | The body does not match the schema, or Idempotency-Key is missing |
| 401 | The key is missing, wrong, or revoked |
| 403 | The key lacks sessions:write, or the caller (for a personal key, its creator) is outside the agent and the workspace's agent_outsider_access is not use (details.reason_code is role) |
409 IDEMPOTENCY_CONFLICT | The same key with a different body |
409 PROFILE_BLOCKED | An owner blocked the operator profile. For a workspace profile, its base is blocked (details.reason_code is base_blocked). Sending it again gets the same answer |
| 413 | The message exceeds 32,768 bytes. STORAGE_LIMIT_EXCEEDED if the storage limit is exceeded |
403 GITHUB_REFUSED | The repository_id you named is outside the agent's GitHub settings list or the workspace ceiling (details.reason_code is repository_not_allowed) |
422 UNSUPPORTED_CAPABILITY | The profile or repository does not exist, the pair is not allowed, or the profile does not allow the model |
429 RATE_LIMITED | Send again after a short wait |
When execution slots run short, the request is not rejected. The session waits as queued.
The SDK's error classes (APIError, ConnectionError, TimeoutError, and others), retry counts, and timeouts are the same for every call, so they are collected in Client options and errors.
Next steps
- Status and lifecycle: the states of sessions and turns
- Send: send more messages to the same session
- SDK samples: SDK samples by use case: checking keys and the catalog, listing sessions, resuming events, retries, answering questions and permission requests, controls and recovery, push approval and PRs