Skip to main content
A session moves through a fixed lifecycle. You can steer it while it runs and read the result when it finishes. Every follow-up call (sending a message, pausing, cancelling) is addressed to the session’s id. The optional max_steps and max_time_s caps bound how long it runs before the agent is asked for a final answer.
hai run "Top 3 stories on Hacker News?" \
  --agent h/web-surfer-flash
SESSION=$(curl -s -X POST https://agp.eu.hcompany.ai/api/v2/sessions \
  -H "Authorization: Bearer $HAI_API_KEY" -H "Content-Type: application/json" \
  -d '{"agent": "h/web-surfer-flash", "messages": [{"type": "user_message", "message": "Top 3 stories on Hacker News?"}]}' | jq -r .id)
echo "$SESSION"
from hai_agents import Client

client = Client()

session = client.sessions.create_session(
    agent="h/web-surfer-flash",
    messages=[{"type": "user_message", "message": "Top 3 stories on Hacker News?"}],
)
print(session.id)
import { HaiAgentsClient } from "hai-agents";

const client = new HaiAgentsClient();

const session = await client.sessions.createSession({
  body: {
    agent: "h/web-surfer-flash",
    messages: [{ type: "user_message", message: "Top 3 stories on Hacker News?" }],
  },
});
console.log(session.id);
Creating a session returns its id, which you poll for progress and the answer as in the Quickstart. For a one-shot run, the helper below blocks until the agent finishes and hands back the result.
result = client.run_session(
    agent="h/web-surfer-flash",
    messages="Top 3 stories on Hacker News?",
)
print(result.status, result.answer)
const result = await client.runSession({
  agent: "h/web-surfer-flash",
  messages: "Top 3 stories on Hacker News?",
});
console.log(result.status, result.answer);
Pick the call that matches how much control you need:
You want to…SDK callYou get back
Run and read the answer in one shotclient.run_session(...) / runSessionBlocks, then returns the final result
Watch or steer while it runsclient.start_session(...) / startSessionA handle bound to the id; read and steer, then wait_for_completion
Drive the loop yourself (raw HTTP, other languages)POST /sessions, then long-poll changesThe session id; you poll

Reading the answer

The simplest is the session snapshot. GET /sessions/{id} returns latest_answer, the agent’s most recent final answer, or null until it first answers. It needs no cursor and never goes stale, so once a run has settled it is the easiest way to read the answer. While a run is active, GET /sessions/{id}/changes carries the same answer alongside the live event feed. Because changes returns only what is new since your cursor (and 204 No Content when nothing new has arrived), the answer rides the page that delivers the final events. Keep polling until the session reaches a terminal state and you have drained the remaining events; a 204 means no new events yet, not no answer. The SDK helpers (run_session / wait_for_session) run this loop and drain to the end for you. Don’t poll status for the answer: it never carries one.

Session object

FieldDescription
idThe session’s UUID, the handle for every follow-up call.
requestEchoes what you submitted, with agent resolved to its full spec even if you passed a catalog id.
statusCarries the live status, step count, per-model token usage (usage_per_model), any error and its error_code, the agent’s self-assessed outcome, and subagent_session_ids. See Session status for the breakdown.
agent_view_urlLink to the session’s Agent View page for live viewing and replay.
latest_answerThe agent’s most recent final answer, mirrored from changes; null until it first answers.
created_at / started_at / finished_atTrack the run’s timeline; the latter two are null until they happen.

Lifecycle

Every session moves through the same state machine, whichever agent runs it:
StatusMeaningTerminal
queuedSession accepted above your concurrency limit; it starts automatically, oldest first, as slots free up.No
pendingSession created, agent is launching.No
runningAgent is actively working on the task.No
pausedManually paused via the API. State is preserved.No
idleInteractive agent finished a task and is waiting for your next message.No
awaiting_tool_resultsAgent is blocked on custom tool calls your code must answer.No
completedAgent finished the task successfully.Yes
timed_outAgent exceeded the maximum allowed time.Yes
interruptedSession was canceled via DELETE.Yes
failedAn unrecoverable error occurred.Yes

Overrides

Reuse a catalog agent but adjust it for a single run with overrides, a map on the create-session body. Rather than defining a new agent, you point at fields of the resolved request. Each key is a dotted path, and its value replaces whatever that path resolves to, applied after agent is expanded from its catalog id.
  • Dots walk into objects. agent.instructions sets behavior, agent.model swaps the serving model, and agent.answer_format pins a structured answer.
  • A [field=value] selector picks a list member. agent.environments[kind=web] selects the web environment, so agent.environments[kind=web].start_url sets just its start page and agent.environments[kind=web].mode switches how it reads the page.
  • Values are type-checked. Each value must match the type of the field its path targets. An unknown path or a wrong type is rejected with 422 at creation, before the agent runs.
For example, send a catalog web-surfer to a chosen page instead of its default start URL:
hai run "Summarize the top discussion right now" \
  --agent h/web-surfer-flash \
  --override 'agent.environments[kind=web].start_url=https://news.ycombinator.com'
curl -X POST https://agp.eu.hcompany.ai/api/v2/sessions \
  -H "Authorization: Bearer $HAI_API_KEY" -H "Content-Type: application/json" \
  -d '{
    "agent": "h/web-surfer-flash",
    "messages": [{"type": "user_message", "message": "Summarize the top discussion right now"}],
    "overrides": {"agent.environments[kind=web].start_url": "https://news.ycombinator.com"}
  }'
session = client.sessions.create_session(
    agent="h/web-surfer-flash",
    messages="Summarize the top discussion right now",
    overrides={"agent.environments[kind=web].start_url": "https://news.ycombinator.com"},
)
const session = await client.sessions.createSession({
  body: {
    agent: "h/web-surfer-flash",
    messages: "Summarize the top discussion right now",
    overrides: { "agent.environments[kind=web].start_url": "https://news.ycombinator.com" },
  },
});

Structured output

By default the agent’s answer is free-form text. Set an answer_format (a JSON Schema) on the agent, or pass a Pydantic / Zod schema to the SDKs, and the final answer comes back as typed, validated data instead. See Structured output.

Listing and filtering

GET /api/v2/sessions pages through your sessions, newest first, with filters you can combine:
FilterTypeDescription
statusstring (repeatable)Filter by session status (e.g. ?status=running&status=queued).
agentstring (repeatable)Filter by agent identifier (e.g. h/web-surfer-flash).
group_idstringFilter by group: useful for multi-session workflows.
parent_session_idstringFind child sessions of a parent.
schedule_idstringSessions created by a schedule’s fires.
searchstringCase-insensitive match on the first message or answer.
created_before / created_afterstringBound by creation time (ISO 8601).
finished_before / finished_afterstringBound by finish time (ISO 8601).
ownerstringAccess scope. Default: me-in-organization.
curl "https://agp.eu.hcompany.ai/api/v2/sessions?status=running&agent=web-price-finder" \
  -H "Authorization: Bearer $HAI_API_KEY"
page = client.sessions.list_sessions(status=["running"], agent=["web-price-finder"])
for summary in page.items:
    print(summary.id, summary.status)
const page = await client.sessions.listSessions({
  status: ["running"],
  agent: ["web-price-finder"],
});
for (const summary of page.items) {
  console.log(summary.id, summary.status);
}
Like every list endpoint, it returns a page envelope: items holds the resources, page echoes the page number you asked for, and total counts all matches. Responses don’t echo size back, so track it yourself; there are more pages while page * size < total. This endpoint caps size at 100 and sorts by -created_at (newest first) unless you pass sort=created_at. See List sessions for the full parameter reference.

Endpoints

MethodPathDescription
POST/api/v2/sessionsCreate a session
GET/api/v2/sessionsList sessions
GET/api/v2/sessions/{id}Retrieve a session
GET/api/v2/sessions/{id}/statusGet session status
DELETE/api/v2/sessions/{id}Cancel a session
POST/api/v2/sessions/{id}/messagesSend a message
POST/api/v2/sessions/{id}/tool_resultsSend tool results
POST/api/v2/sessions/{id}/pausePause a session
POST/api/v2/sessions/{id}/resumeResume a session
POST/api/v2/sessions/{id}/force_answerForce final answer
GET/api/v2/sessions/{id}/changesLong-poll for changes
GET/api/v2/sessions/{id}/eventsList events
GET/api/v2/sessions/quotaGet quota
POST/api/v2/sessions/{id}/feedbackSubmit session feedback
POST / DELETE/api/v2/sessions/{id}/shareShare / unshare a session
GET/api/v2/sessions/{id}/resources/{bucket}/{key}Get a session resource