Tasks and Sessions
Tasks are the managed coding API. A task is the work item. A session is one agent run under that task. Messages are the conversation with the agent. Attachments provide extra context.
Use this workflow when you want Wallfacer to boot the VM, run the agent, manage idle teardown, and preserve the conversation.
Task vs Session
| Concept | Responsibility |
|---|---|
| Task | The work item, owner, default environment, original prompt, attachments, and completion state. |
| Session | One attempt against the task: environment, VM, branch name, base branch, messages, and idle timeout. |
| Message | A user instruction, assistant response, or internal transcript event. |
| Attachment | Context carried with the task in MCP resource shape. |
A task can have multiple sessions. That is useful when you want separate attempts, separate branches, or sibling sessions in different environments.
Create A Task That Starts Work
If you create a task with a prompt, Wallfacer creates a session, sends the first message, and begins booting a VM.
curl -s -X POST "$WALLFACER_API/accounts/$ACCOUNT_ID/tasks" \
-H "Authorization: Bearer $WALLFACER_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: task-checkout-flake-001" \
-d '{
"title": "Fix checkout test flake",
"prompt": "Find and fix the flaky checkout test. Keep the change focused and add a regression test.",
"environment_id": "'$ENVIRONMENT_ID'",
"idle_timeout_seconds": 600,
"attachments": [
{
"name": "CI failure",
"uri": "https://github.com/acme/shop/actions/runs/123",
"mimeType": "text/markdown",
"text": "The checkout test fails intermittently in CI with a timeout waiting for payment confirmation."
}
]
}' | jq ".data"environment_id is optional. When you omit it, the task runs on its agent's computer: it defaults to the environment configured on the task's agent (resolved through created_by, see Accounts and Members). An explicit environment_id always wins. Omit it and leave the agent without an environment only when the task spans multiple environments, then set environment_id per session instead.
The task response gives you the task ID. List sessions for that task to find the generated session.
Choose A Harness, Model, And Effort
Set harness, model, and effort to choose which coding agent runs the work, which model it uses, and how much reasoning effort it spends. All three are optional, and all three apply to the session that task create starts when you also pass a prompt. A create without a prompt starts no session and drops them.
They are not stored on the task. Creating a session under an existing task accepts none of these fields, and that session resolves its harness, model, and effort from the acting agent's current settings.
harness accepts claude or codex and defaults to claude. model is a model id scoped to that harness, such as claude-opus-4-8 for Claude or openai/gpt-5.6-sol for Codex. Pass the id exactly as GET /v1/models reports it. effort is in the chosen harness's own vocabulary: low, medium, or high for Claude, and minimal, low, medium, high, or xhigh for Codex. When you omit effort, the effort pinned on the task's agent applies only if harness resolves to that agent's own harness. Send a different harness and the agent's effort is ignored, because each harness has its own vocabulary, and the selected harness's default applies instead.
An unknown harness, model, or effort falls back to a supported default rather than failing the request, so read session.vendor, session.harness_model, and session.harness_effort to confirm what the session runs. See Models for the available options.
curl -s -X POST "$WALLFACER_API/accounts/$ACCOUNT_ID/tasks" \
-H "Authorization: Bearer $WALLFACER_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: task-codex-sol-001" \
-d '{
"title": "Fix checkout test flake",
"prompt": "Find and fix the flaky checkout test.",
"environment_id": "'$ENVIRONMENT_ID'",
"harness": "codex",
"model": "openai/gpt-5.6-sol",
"effort": "high"
}' | jq ".data"curl -s "$WALLFACER_API/accounts/$ACCOUNT_ID/tasks/$TASK_ID/sessions" \
-H "Authorization: Bearer $WALLFACER_TOKEN" \
| jq ".data[0]"Create Additional Sessions
Create another session when you want a separate attempt or branch.
curl -s -X POST "$WALLFACER_API/accounts/$ACCOUNT_ID/tasks/$TASK_ID/sessions" \
-H "Authorization: Bearer $WALLFACER_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: session-checkout-flake-alt-001" \
-d '{
"title": "Alternative checkout fix",
"prompt": "Try a minimal fix in the polling helper only.",
"environment_id": "'$ENVIRONMENT_ID'",
"branch_name": "fix/checkout-flake-alt",
"base_branch": "main",
"auto_commit": true
}' | jq ".data"Sessions inherit the task environment when environment_id is omitted. Pass a session-level environment when one task spans multiple repos or stacks.
Session States
Sessions move through lifecycle states:
| Status | Meaning |
|---|---|
waiting | No machine was free when the session needed one. It is queued and retries on its own, and waiting_since reports when the wait started. Messages sent meanwhile are held and delivered once it starts. |
waiting_for_snapshot | The environment base snapshot is generating for the first time. |
starting | A VM is provisioning. |
active | The VM is up and the agent can receive messages. |
idle | The idle timeout elapsed and the VM was destroyed; sending a message can reactivate it. |
closed | The session was closed or failed terminally. |
Subtract waiting_since from the current time to show how long a queued session has been waiting. It is non-null only while status is waiting, and it clears as soon as the session starts, closes, or goes back to idle. A session that entered the wait before the field existed reports null while still waiting, so read null as an unknown duration rather than as evidence the session is not waiting. status is the authority on that.
Use close_reason to explain why a session stopped. It distinguishes user-initiated close, successful completion, VM failure, setup failure, snapshot failure, turn failure, resource exhaustion, and related states.
Read close_reason together with status, not instead of it. Two reasons are automatic recoveries and leave the session idle rather than closed, so a non-null close_reason does not by itself mean the session is finished:
| Reason | What happened | Resulting status |
|---|---|---|
provisioning_failed | The session never finished starting up and was recovered automatically. | idle. Send another message to retry. |
harness_lost | The agent stopped without reporting how the turn ended, and the session was recovered automatically. | idle. Send another message to retry. |
resource_exhausted | No machine ever came free. The session waited and retried for over an hour before giving up. | closed |
waiting_timeout | The session sat queued for longer than any retry schedule allows, so it was closed rather than left waiting indefinitely. | closed |
Both recovery reasons set error_message with what to do next, and both preserve the transcript.
Waiting for Capacity
A session that cannot get a machine reports the wait, so a client can render it instead of showing a slow boot:
statusiswaitingandwaiting_sinceis the ISO 8601 timestamp the wait began. Subtract it from the current time for elapsed wait. It is null outside a wait, and a wait that started before the timestamp was recorded reports null, so read null as an unknown duration rather than as evidence the session is not waiting.- The parent task's
binding_constraintreportskind: "retrying", withnext_retry_seconds(the wait before the next attempt) andretries(attempts so far). It carries the same shape as the constraint a capacity-parked pipeline step emits, so one rendering path covers both. error_messageon a capacity failure is written for a person to read and says what to do next. It no longer carries the upstream provider payload.
The platform retries for over an hour and then closes the session with close_reason: resource_exhausted. A wait that outlives any retry schedule is swept and closed with close_reason: waiting_timeout. Both are resumable: posting a message starts a fresh attempt.
Describing A Task In A List
last_message_preview on the task is a one-line, markdown-stripped summary of the last thing the agent said on it, across all of its sessions. Read it when a list row should describe the work rather than restate the lifecycle status: status: "active" reports only that the task has not reached a terminal state, which a plain conversation never does.
- Only turns carrying visible text count, so a turn that only called tools or only thought leaves the previous summary standing.
- It is
nullwhen the agent has not spoken on the task yet. Render the row without a second line rather than printing a placeholder. - The summary is stored on the task, so listing tasks costs no request per row.
Send Follow-Up Messages
curl -s -X POST "$WALLFACER_API/accounts/$ACCOUNT_ID/tasks/$TASK_ID/sessions/$SESSION_ID/messages" \
-H "Authorization: Bearer $WALLFACER_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: msg-add-test-001" \
-d '{"message": "Also add a unit test around the timeout branch."}' \
| jq ".data"Sending a message automatically resumes an idle or closed session when the platform can reactivate it.
Messages are ordered by sequence when delivered. Queued messages can have sequence: null, so clients should render by sequence and then creation time.
Conversation UIs usually render user and assistant messages and collapse internal types such as queue-operation, attachment, and ai-title.
Message lists are cursor-paginated. Use meta.next_cursor when present to load the next page.
Read Newest Messages First
A message list comes back oldest first. Pass direction=desc to start at the newest message instead, which is what a client that opens a conversation on its last screen needs.
curl -s "$WALLFACER_API/accounts/$ACCOUNT_ID/tasks/$TASK_ID/sessions/$SESSION_ID/messages?direction=desc" \
-H "Authorization: Bearer $WALLFACER_TOKEN" \
| jq ".data"The cursor walks in whichever direction the page was fetched in, so following meta.next_cursor from a desc page pages backward into older messages.
asc is the default, so a request that sends no direction gets the order it always got. Any other value is a 422 with a direction field error.
Attach Context
Attachments are MCP-aligned resources. For agent context, include inline text or base64 blob. Use uri as a reference to the source of the content; do not assume the agent will fetch arbitrary external URIs for you.
curl -s -X POST "$WALLFACER_API/accounts/$ACCOUNT_ID/tasks/$TASK_ID/attachments" \
-H "Authorization: Bearer $WALLFACER_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: attachment-ticket-42" \
-d '{
"name": "PROJ-42",
"uri": "https://tracker.example.com/PROJ-42",
"mimeType": "text/markdown",
"text": "# PROJ-42\nCheckout sometimes times out while waiting for payment confirmation."
}'Attachments report status: "ready" when content is available and status: "failed" when content could not be made available. Read attachment contents through the contents route when you need MCP ResourceContents shape.
Stop Or Close Work
Abort a running session when you want to interrupt the agent without permanently closing the session:
curl -s -X POST "$WALLFACER_API/accounts/$ACCOUNT_ID/tasks/$TASK_ID/sessions/$SESSION_ID/abort" \
-H "Authorization: Bearer $WALLFACER_TOKEN"Close a session when the attempt is done:
curl -s -X PATCH "$WALLFACER_API/accounts/$ACCOUNT_ID/tasks/$TASK_ID/sessions/$SESSION_ID" \
-H "Authorization: Bearer $WALLFACER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"status": "closed"}'The session VM is released when the session idles or closes.
Ownership In Automation
A task runs as an agent. created_by is that execution identity: it must be an enabled agent of the account, and it defaults to the account's connected agent when you omit it. Every commit, pull request, and credential the task uses resolves to that agent, not to a person.
The responsible human is recorded separately as the task's owner. When a person's token creates a task, that person becomes the owner; a task an agent opens on its own stays unowned and surfaces in a shared "needs someone" queue. Task access is account-scoped, so any member can list and read the account's tasks regardless of who created them.