Open the console

Concepts

Invocation lifecycle

There is no HTTP endpoint per agent. An agent is a queue consumer: you publish an invocation, a pod picks it up, and everything it does comes back as events. This is the whole path, in order, and the places along it where a run can stop before your code has run at all.

The whole path

Your code posts an invocation to rt-service, which checks it and publishes it to the target's own queue, answering 202 with an invocation id. A pod is woken if it was scaled to zero, pulls the message, and runs it through the plugin stack and then your agent. Every event is published as it happens and reaches your open socket; a refused call drops out at rt-service before anything is queued.to every socket watching that sessionYour codePOST /v1/invocationsrt-servicechecks, publishesThe queueone per targetYour podplugins, then ADKEventsas they happenthe invocation202, then queuedwakes and pullspublishedrefused — nothing is queued400, 402, 403, 502
Your code posts an invocation to rt-service, which checks it and publishes it to the target's own queue, answering 202 with an invocation id. A pod is woken if it was scaled to zero, pulls the message, and runs it through the plugin stack and then your agent. Every event is published as it happens and reaches your open socket; a refused call drops out at rt-service before anything is queued.

1. Publish

POST /v1/invocations answers 202 with an invocation id, not a result. Before it publishes, rt-service refuses what it can see is wrong: a sibling project or a missing admin role for end_user_id is a 403, a state key or run option you may not set is a 400 naming it, and a tenant suspended for an unpaid balance is a 402. Refusing there is what stops work piling up against a consumer that does not exist and expiring silently a week later.

Then it publishes to the target's own topic, agent.invoke.<tenant>.<project>.<target>, and sends a queued event straight to your socket. A target that does not exist has no topic, so the publish itself fails and the call answers 502 could not queue the invocation — which reads like an outage and usually means a misspelled slug or a missing project.

Subscribe before you invoke

rt-service starts reading your tenant's events when your first socket opens, and nothing buffers them for a socket that is not there. A client that invokes and then connects misses everything published in between — on a warm agent, that is the entire reply. The run is still recorded, and its record can be read back afterwards, but the live stream does not replay. The SDK's run does the subscribe-then-invoke ordering for you.

2. Scale from zero

minReplicas: 0 is the default, so most agents have no pod when you invoke them. The autoscaler watches two signals: rt-service records every publish the moment it answers 202, which wakes the target within one ten-second poll, and the queue's own backlog takes over once the cloud's metrics catch up a few minutes later. A burst of invocations asks for one replica per recent invocation, up to maxReplicas.

A cold start is then a pod being scheduled, its sandbox starting, Python importing your code and build() running. Measured over 24 sandboxed pods on the non-production cluster: a median of 13.2 seconds and a 95th percentile of 21.9. That holds while a sandboxed node exists; the platform keeps one placeholder pod holding such a node up for the whole cluster, because a pod that has to wait for a node took over a minute and a half. A workload goes back to zero once it has been quiet for several minutes.

Nothing about this is configurable per invocation. If an agent must be warm, set scaling.minReplicas: 1 in elan.yaml and pay for the reservation — compute is billed on what a pod reserves, not on what it uses.

3. Delivery, and the lease

The pod pulls from its own subscription, at most limits.max_concurrent invocations at once, so the queue rather than the pod's memory holds the backlog. Delivery is ordered by session: a second message for one conversation waits until the first is done, because two concurrent turns on one session would make one of them fail after its model had already been paid for. Different conversations still run in parallel.

A message that can never succeed — malformed, or addressed to a different target — is set aside on a dead-letter queue and acknowledged, rather than retried: redelivering it would only fail again, and spend the retry budget doing it. A pod that is full hands the message back to be redelivered. Anything else that fails unexpectedly is retried, up to five deliveries, and then dead-lettered.

timeout_s does not cancel a run

The pod keeps the message leased while a run works, and waits for it for limits.timeout_s plus a minute. Past that it hands the message back to the queue, which redelivers it — while the first run carries on, because nothing stops it. So a run that outlasts that window can run a second time. Set timeout_s well above the longest turn you expect, and bound a run with max_llm_calls, which does stop one. A run that has to wait for a person does not need either: it pauses, and the answer arrives as a new message.

4. The plugin stack

Every run passes through the platform's plugins before your agent sees it, in this order. They are ordinary ADK plugins, and the framework stops at the first callback that returns a value — which is why the two gates come first and anything of yours comes last.

  1. The ledger. Opens a hold for the manifest's deposit_micros before the run starts; a payer who cannot cover it is refused here, before your code executes. After each model response it tallies what has been spent, and past 80% of what is held it reserves the same amount again. If that top-up has no credit behind it, the next model call is skipped — the only hook that can prevent spend, since by the time a response arrives the tokens are bought. So a run can exceed a balance by at most one model call. A target declared unbilled has no ledger plugin at all.
  2. Autonomy. Resolves every call to a declared tool to refuse, ask or allow, against that call's arguments; see autonomy and action policy. A refused tool never reaches anything that meters or announces it.
  3. MCP Apps. Turns an MCP tool's declared view into a surface event. The framework does nothing with that declaration by itself.
  4. Attachments. Before each model call, appends any file the user's own turns name to the request, as a real file; see files and multi-modal.
  5. Typing. Emits the typing events a chat shows — thinking, running a tool — worded from the phrasing your manifest gives each tool.
  6. Observability. Writes one summary line per run to the pod's log. The run record you read back is built elsewhere, from the events.
  7. Your own plugins, in the order your manifest lists them — after every gate, so a plugin of yours cannot skip a hold or a policy decision, and see plugins for what one may do.
  8. The tool meter, last of all, counting priced tool calls the whole stack let through.

A callback is found by its name

The framework calls a plugin's callbacks by method name, so a misspelled one does not fail — it silently never fires. And a plugin holding state for a run needs all three error hooks, because the after-run callback fires only for a run that did not raise.

5. The run

The framework's runner executes the agent or graph your build() returned, with at most limits.max_llm_calls model calls — or fewer, if the invocation's run_config asked for fewer. Inside its sandbox your agent has:

  • its own database, at DATABASE_URL, which also holds its conversations;
  • its own files, in the scopes it may reach;
  • the secrets you named, as environment variables;
  • network to the hosts you declared, and nothing else;
  • no long-lived cloud credentials — what the pod may touch is granted to its own identity, not handed to it as a key.

6. Events out

Everything the run does is published to your tenant's event topic as it happens, in order within each session, and three things read it: rt-service, which fans each event out to whoever holds a socket watching that session; the run record, which is what GET /v1/runs/{invocation_id} reads back; and the approval inbox. The socket is the live path and never the durable one — a client that disconnects has lost the live events, and reads the run or the conversation back instead.

The event envelope

Every frame has the same shape, live or read back from a conversation's history:

{
  "invocation_id": "f27bfbb4a1c34e...",
  "target_slug": "invoice-triage",
  "target_version": "invoice-triage@1.4.0+…",
  "target_kind": "agent",
  "tenant_id": "acme",
  "project_id": "agents",
  "user_id": "eu:ada",
  "session_id": "web-9a3f1b2c4d5e",
  "segment": "",
  "seq": 3,
  "kind": "content",
  "payload": { "author": "invoice_triage",
               "content": { "role": "model", "parts": [{ "text": "…" }] } },
  "emitted_at": "2026-09-28T09:14:02.481Z"
}
  • invocation_id is what to filter on, because a session carries several runs.
  • segment says which part of an invocation produced the event: empty for the original run, and the interrupt id for each resume. A resumed run starts its seq again at 1, so seq orders events only within a segment.
  • A content payload is the framework's own event, dumped whole. The text is at payload.content.parts[*].text — join the parts rather than taking the first, because a reply that arrives in several parts is otherwise truncated with nothing to say so. payload.author says which agent spoke.
  • Synthetic and terminal events carry flat strings instead — payload.text, message, error. Both shapes travel on one topic, so read both.

Event kinds

kindMeaning
queuedAccepted and published. Nothing is running yet. Sent by rt-service, because a scaled-to-zero agent has no pod to send it.
startedA pod began the run. Carries the message that started it and stamps the agent's version.
contentThe agent said something.
tool_callIt called a tool.
tool_resultThe tool returned.
typingLiveness for a chat UI — thinking, running a tool, idle. Cheap and frequent.
surfaceA view the agent wants shown, from an MCP tool that declared one.
action_requestedA tool call is waiting for a person to confirm it, or was decided by policy.
action_resolvedThat action was approved, rejected, or abandoned because the conversation moved on.
pausedThe run is waiting on a person or an external system. The last one carries interrupt_id, and then nothing more comes.
finishedThe run completed. Terminal.
errorThe run failed. Terminal, and nothing follows it.

A paused run never sends finished

It emits two paused events — and there can be a content event between them — and only the last carries interrupt_id, interrupt_kind and the prompt as message. That key is what tells a stopped run from one still going; the SDKs' ends_the_run and endsTheRun spell the rule once. A client that waits for finished waits out its whole timeout on a run that has been asking it a question all along.

7. Settlement

When a run ends the ledger settles its hold at the real figure — tokens at your prices plus any priced tool calls — and releases whatever the deposit over-reserved. That happens on a pause too: what the turn used so far is settled, and the resume reserves again on its own merits.

A run that raises — a model error, a tool that threw, the runner failing, or the run reaching max_llm_calls — is released instead: the reservation is freed and nothing is charged for the tokens it used. A pod that dies outright releases nothing; its hold expires and a sweep reclaims it, typically within half an hour, and until then that credit is unavailable to the payer's next run.

Paused runs: how long, and what they cost

A confirmation or a workflow's request for input has no time limit; what ends an unanswered confirmation is the conversation moving on, which records it abandoned, and a late approval of one is refused rather than run. A long-running tool call waits at most the agent's limits.tool_deadline_s — a day by default — and an approval request at most the deadline it was given; past either, the run resumes with the call reported as never answered, so the agent can say so.

Waiting costs nothing, because the pause settled the turn and holds no reservation. But the resume reserves again, so a resume can be refused for funds even though the original run succeeded: the answer is accepted, and the resumed run's reply is the insufficient-balance message. Top up and answer again.

Do not hold a socket open across a pause. Nothing buffers events for a socket that is not connected, so waiting gains nothing. Keep the invocation id, let the socket go, and list what is waiting on you with GET /v1/runs?status=paused — the one call that lists both kinds of pause, a workflow input and a tool confirmation.

Where a run can stop

In order along the path, so you can tell which one you are looking at:

StopWhoWhat you see
A sibling project, or end_user_id without adminrt-service403, nothing queued
A state key or run option you may not setrt-service400 naming it, nothing queued
Tenant suspended for an unpaid balancert-service402, nothing queued
No such targetrt-service502 could not queue the invocation
An agent image too old for the invocationthe podqueued, then nothing — dead-lettered; redeploy
The payer cannot cover the deposit, or the wallet is frozenthe ledger plugina reply saying so, then finished; your code never ran
Credit runs out mid-runthe ledger pluginthe next model call is skipped with the same message
A tool refused by policyautonomythe tool does not run; the agent explains why and carries on
Waiting for a personautonomy, your tool or your graphpaused with an interrupt id
max_llm_calls reachedthe frameworkerror
Your code raisedyouerror, with the traceback in the logs

elan logs --workload <name> is where the last of those lives.

Where to go next

The invocations API is the two ends of this path — starting a run, watching it, resuming it and reading it back — and the runtime SDK wraps them with the ordering done for you. Billing and the ledger is the hold in more depth, and troubleshooting starts from the symptom rather than the path.

Reading this as a model? llms.txt indexes every page, and each page has a “Copy for AI” control.