Short answer: Pass aninitial_eventsarray onPOST /v1/sessionsand the session is created directly inrunningstatus — the agent loop starts in the same call, so you skip the second request to the events endpoint. It takes up to 50user.messageanduser.define_outcomeevents, validation is all-or-nothing, and the seeded events aren't echoed back, so read them from the session's event list.
A Claude Managed Agents session has always followed a two-step lifecycle: create the session, then send a user event to start work. That's a clean state machine, but for the common case — you already know the first thing you want the agent to do — it costs you a round-trip you didn't need. A July 22, 2026 change closes that gap with an initial_events parameter.
Before: create, then send#
The classic two-call pattern. Create the session, capture its id, then post the first event:
# 1) create an idle session
session=$(curl -fsSL https://api.anthropic.com/v1/sessions \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: managed-agents-2026-04-01" \
-H "content-type: application/json" \
-d "{\"agent\":\"$AGENT_ID\",\"environment_id\":\"$ENVIRONMENT_ID\"}")
SESSION_ID=$(jq -r '.id' <<< "$session")
# 2) hand it work — this is what actually starts the agent loop
curl -fsSL "https://api.anthropic.com/v1/sessions/$SESSION_ID/events" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: managed-agents-2026-04-01" \
-H "content-type: application/json" \
-d '{"events":[{"type":"user.message",
"content":[{"type":"text","text":"List the files in the working directory."}]}]}'
After: seed at creation#
Move the first event into initial_events on the create call. A non-empty list creates the session directly in running status and starts the agent loop inside that same request, so there is no follow-up call to the events endpoint at all. The body is the same as a plain create, plus one initial_events array carrying your opening user.message:
curl -fsSL https://api.anthropic.com/v1/sessions \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: managed-agents-2026-04-01" \
-H "content-type: application/json" \
-d @- <<EOF
{
"agent": "$AGENT_ID",
"environment_id": "$ENVIRONMENT_ID",
"initial_events": [
{
"type": "user.message",
"content": [{"type": "text", "text": "List the files in the working directory."}]
}
]
}
EOF
Same thing in the SDKs — pass initial_events to sessions.create:
seeded = client.beta.sessions.create(
agent=agent.id,
environment_id=environment.id,
initial_events=[
{"type": "user.message",
"content": [{"type": "text", "text": "List the files in the working directory."}]},
],
)
const seeded = await client.beta.sessions.create({
agent: agent.id,
environment_id: environment.id,
initial_events: [
{ type: "user.message",
content: [{ type: "text", text: "List the files in the working directory." }] }
]
});
Reading the seeded events back#
The one surprise: initial_events are not echoed on the create response. They're persisted with server-assigned ids exactly as if you'd posted them to the events endpoint — you just have to list them:
for event in client.beta.sessions.events.list(seeded.id):
if event.type == "user.message":
for block in event.content:
if block.type == "text":
print(f"Seeded event: {block.text}")
Seed the goal too, not just the prompt#
initial_events isn't limited to a plain message. You can seed a user.define_outcome event in the same call, so the agent starts with both its task and the rubric it will be graded against. Two rules keep you out of a 400:
- At most one
user.define_outcomeper list. - Every
user.define_outcomemust carry arubric— see Define outcomes for the shape.
That's the highest-leverage use of the feature: one request that spins up the session, states the task, and pins the success criteria, all before the sandbox is even provisioned.
The gotchas, in one place#
- Only two event types.
user.messageanduser.define_outcome. Response events (user.tool_confirmation,user.tool_result,user.custom_tool_result) anduser.interruptare rejected — there's no agent turn yet to answer or stop. And unlike a scheduled deployment'sinitial_events, a session's list won't acceptsystem.message. - All-or-nothing validation. If any event in the list is invalid, the entire request fails and no session is created. You won't get a half-started session to clean up.
- The hard limits. Up to 50 events; at most one
user.define_outcome; no more than 100 file-sourceddocumentblocks across the whole list; body under 32 MB (or a 413). - Empty means nothing.
initial_events: []is treated the same as omitting the field — you get an idle session, not a running one. Only a non-empty list starts the loop.
When to reach for it#
Use initial_events whenever you already know the opening move: a one-shot task, a cron-style kickoff, a fan-out where each session gets a fixed brief — and if each of those sessions needs a different model or tool set, pair it with per-session config overrides in the same create call. Stick with create-then-send when you genuinely need the session id before you can compose the first message — for instance, when the task text references the session itself. For everything else, it's a free round-trip back in your latency budget, and a cleaner call site: the session and its first instruction arrive together.



