Create
Define an agent, create and activate a release, and bind it to a session
An agent is a named definition in a workspace. Name and description are values for people; the definition a run uses is a card (model, tools). Every edit to the card adds a new version. A release binds one version to a profile, and the release an agent points to is its active release. When you pass a release while creating a session, that session belongs to that agent.
Sessions run with the profile named by profile_id, and a session created from a release uses only the tools pinned to that release. Name and description are not passed to the model; how the agent should work is told by the repository's AGENTS.md or CLAUDE.md. See Instructions and description and Permissions for details.
In the web console
The web console of the local stack is at http://127.0.0.1:3000/.
- The first time only, create the first owner and workspace at
/bootstrap. How you get the first owner's token depends on the installation. On a single-server installation where you did not set the token yourself,scripts/test-ops.sh bootstrap-tokenshows it once. See first owner and keys for details. - After logging in, open "Agents" in the left sidebar and click "New agent" at the top right. The "New agent" screen opens with five steps: Name → What it does → Release → Turn on → Connect channels.
- In the "Name" step, enter a name. The identifier (slug) is filled in from the name, and you can change it. If you like, write a short introduction for people in "Description (optional)". Click "Next".
- In the "What it does" step, choose the model, check the profile's tools under "Tools it may use" (Instructions and description), set the reply language, then click "Save draft and continue". The agent is saved as a draft and version 1 is created. Instructions for how it should work go in the repository's
AGENTS.mdorCLAUDE.md. - In the "Release" step, choose a profile and click "Create release". The card's model must be one the profile can run.
- In the "Turn on" step, click "Turn the agent on". New sessions now start with this release. "Do this later" leaves the agent as a draft and opens its detail screen.
Owners, admins, and members can all create agents, and the person who creates one becomes that agent's administrator.
Through the API
The agents API accepts only the same cookie login as the web console. Calling it with an API key returns 403.
Log in
read -rs WERFT_PASSWORD
WERFT_COOKIE=$(curl -sS -o /dev/null -D - "$API/v1/auth/login" \
-H 'Content-Type: application/json' -H 'X-Requested-With: werft-web' \
-d "$(jq -nc --arg e 'owner@example.test' --arg p "$WERFT_PASSWORD" '{email: $e, password: $p}')" \
| sed -n 's/^[Ss]et-[Cc]ookie: \(__Host-werft_session=[^;]*\).*/\1/p')The login cookie has a __Host- name and is Secure, so curl's cookie jar does not store it over http. Take it from the response headers and send it in a Cookie header. Requests that change state need X-Requested-With: werft-web.
WEB=(-H 'Content-Type: application/json' -H 'X-Requested-With: werft-web' -H "Cookie: $WERFT_COOKIE")
curl -sS "$API/v1/auth/me" "${WEB[@]}" | jq '{role: .principal.role, workspace: .workspace.slug}'Create
An agent must have a responsible team. An owner or admin picks any team from GET /v1/teams; anyone else picks a team of their own from GET /v1/my-teams. The example below creates a team as the owner and uses it (a team of the same name already there is 409; use that team's id).
TEAM=$(curl -sS "$API/v1/teams" "${WEB[@]}" -d '{"name": "Research"}' | jq -r .team.id)
agent=$(curl -sS "$API/v1/agents" "${WEB[@]}" -d '{
"slug": "research-bot",
"name": "Research Bot",
"description": "Reads the repository and answers questions",
"managing_team_id": "'"$TEAM"'",
"card": {
"model": "claude-sonnet-5",
"capabilities": { "tools": ["Read", "Grep", "Glob"], "mcp": null }
}
}')
AID=$(echo "$agent" | jq -r .agent.id)
VID=$(echo "$agent" | jq -r '.versions[0].id')
echo "$agent" | jq '{slug: .agent.slug, version: .versions[0].version_no, active_release_id: .agent.active_release_id}'{
"slug": "research-bot",
"version": 1,
"active_release_id": null
}If the same workspace already has the same slug, the response is 409 ALREADY_EXISTS. Creating an agent alone does not activate anything.
The team in managing_team_id becomes the responsible team. Leaving it out is 400 BAD_REQUEST, with {"path": "managing_team_id", "message": "managing_team_id is required"} in error.details.issues. An owner or admin who names a team that does not exist gets 404. Anyone else who names a team they are not in gets 403 (details.reason_code: team_not_allowed), even when the team does not exist. So a member who is in no team cannot create an agent. The response's agent.managing_team_name is that team's name.
Every agent has one responsible team. PUT /v1/agents/{id}/managing-team changes it, and nothing clears it. For agents created before the responsible team was required, the migration filled it in: every agent that had none went to a team with no members named Unassigned agents, one per workspace (if a team with that name already exists, an 8-character id follows, as in Unassigned agents (1a2b3c4d)). That team is an ordinary team: rename it, or move its agents to another team.
Create a release
A release binds one version to one profile. If the card's model is not one the profile can run (its model or models), or the card has a tool the profile does not have, the response is 422. A session created with the release runs the card's model unless it names another.
release=$(curl -sS "$API/v1/agents/$AID/releases" "${WEB[@]}" \
-d "$(jq -nc --arg v "$VID" '{version_id: $v, runtime_profile_id: "claude-coding-local"}')")
REL=$(echo "$release" | jq -r .release.id)
echo "$release" | jq '{id: .release.id, created, tools: .release.effective_tools.tools}'{
"id": "sha256:…",
"created": true,
"tools": [
"Read",
"Glob",
"Grep"
]
}The release id is a hash of the card and profile contents. Creating a release again with the same contents returns the same release with created: false.
A note in the request (1 to 200 characters after trimming) is kept on the release. The note is not part of the id, so creating the release again with the same contents keeps the first note. An empty note or one over 200 characters answers 400.
People name a release by its label: the second it was created, in Seoul time, as 2026-10-30_14:30:28. When the same agent gets more than one release in the same second, the second one onward ends in -2, -3, and so on. The console's release history, the agent's header tag and the session lists use this name. The release id is still the identifier. A card's version_no only goes up when the card changes, so releasing the same card with another profile keeps the same version.
The console's release history shows, for each release, what changed against the release the same agent made just before it (allowed repositories, model, tools and MCP, prompt, settings), with who made it and when. Credentials show only as "Changed", never their values. The session screen shows the release the session uses and marks it when it is not the agent's active release. A session keeps the release it started on, so changed settings apply from new sessions. The session response's agent.release_is_active carries that comparison.
Activate
expected_revision is the agent's current activation_revision. It starts at -1. If someone changed it elsewhere first, the response is 409 REVISION_CONFLICT.
curl -sS "$API/v1/agents/$AID/activate" "${WEB[@]}" \
-d "$(jq -nc --arg r "$REL" '{release_id: $r, expected_revision: -1}')" \
| jq '{activation_revision, previous_release_id}'{
"activation_revision": 0,
"previous_release_id": null
}A release's activated_at is when it was last activated, and deactivated_at is when the next activation took its place. The active release has a deactivated_at of null, and a release that was never activated has both null. Both are read from the activation history, so releases activated before these fields existed have them too.
List and count
curl -sS "$API/v1/agents?limit=50" "${WEB[@]}" | jq -c '{n: (.items | length), next_cursor}'
curl -sS "$API/v1/agents/counts" "${WEB[@]}" | jq -c{"n":1,"next_cursor":null}
{"counts":{"all":1,"administered":1,"active":1,"draft":0}}The list returns agents oldest first, limit at a time (default 50, at most 100). Send the response's next_cursor back as cursor for the next page; it is null on the last one. Without cursor you get the first page only. With q (up to 200 characters) you get only agents whose name or slug contains it, ignoring case; with ids, agent IDs joined by commas (up to 100), you get only those agents. With runtime_profile_id you get only agents whose active release runs on that profile; an agent with no active release is left out. In every case the next page comes by the same cursor. GET /v1/agents/counts returns how many agents the list holds read to its end (all), how many of them you administer yourself or through a team (administered), how many are on through an active release (active), and how many are drafts, not archived but with no active release yet (draft). Both need the same permission as the list, and API keys are refused.
The responsible team in the console
The responsible team is the team an agent belongs to. Its team leads manage the agent; its members see the settings and use it (Permissions).
- New agent: the first step requires a responsible team. Owners and admins pick from every team, members from the teams they are in. With a single team it is picked for you. A member in no team sees only “You are in no team, so you cannot make an agent. Ask a workspace admin to add you to a team.” and cannot go on.
- Agent list: the table and the cards show the responsible team. The card of an agent whose settings you cannot read shows only its name, state and responsible team.
- Admins tab: the
Responsible teamgroup at the top shows the current team's name as a link to the team page. Whoever manages the agent picks a team inChangebeside it and it applies at once; it cannot be cleared. A responsible team with no team lead is pointed out. TheAgent adminslist below shows people and teams as the same one-line rows, and choosing from the search results adds at once. When the responsible team is also an admin team, its row says that every member is an admin. - Responsible team members: they read every setting on the detail page but get no edit, release or connect buttons, and a notice at the top says they can only read.
- Outside members: opening the detail address shows the name, state and responsible team with “Only the responsible team's members and the agent's admins see its settings.” instead of the settings.
Bind to a session
Put agent_release_id in the session create request. profile_id must be that release's profile (otherwise 409 RELEASE_PROFILE_MISMATCH). Use an agent key of that agent. Issue an agent key in the web console, or with the web login cookie by sending kind: "agent" and agent_id to POST /v1/api-keys (API key). The SDK has no method for issuing it. Keep the secret in a variable only.
repository_id is optional. If you omit it, an agent with GitHub settings starts without a repository and attaches only what it needs (Sessions without a repository), and an older agent with no settings starts on its release's repository. The example below names sample-app directly and starts on that repository.
import { Werft } from "@werft-ai/sdk";
// WERFT_API_KEY holds the agent key; WERFT_AGENT_RELEASE_ID the release above.
const agent = new Werft();
const started = await agent.sessions.create({
profile_id: "claude-coding-local",
repository_id: "sample-app",
agent_release_id: process.env.WERFT_AGENT_RELEASE_ID ?? "",
message: 'GATE-SPEC {"id":"agent","steps":[],"final":"hello from research-bot"}',
});
console.log(started.status, started.turn_id);A request from a session made with an agent key is approved by another key or a person (A separate key for approvals).
Sending with a key that is not in the workspace (for example scripts/local.sh key quickstart …) returns 404 because the release is not found.
Next steps
- Instructions and description: name, description and the repository's
AGENTS.mdandCLAUDE.md; card fields and versions - Choose a profile: profiles and models
- Permissions: tool permissions and agent administrators