Guides
Troubleshooting
The failures people actually meet, named the way you will see them. Invoking first, then deploying. Most of them are not bugs: they are the platform refusing something on purpose, or a run doing exactly what it was told somewhere you were not looking.
HTTP 502 or could not queue the invocation
The invocation was published to a topic that does not exist — almost always a missing or wrong project, or an agent that is not deployed in the project you named. An absent project means the platform's own, not yours, and on most platforms an unknown agent slug arrives here too rather than as a 404. The CLI prints only the status; the API body says could not queue the invocation.
- Run
elan invokefrom inside your project's directory: it reads the project fromelan.yaml, and from anywhere else it sends none. elan agentslists every agent with its real project.- From your own code, always send
project. The SDK refuses to invoke without one for exactly this reason.
404 unknown target, or no such thing: /v1/invocations
The slug is not one the platform knows. You see this only on a platform that keeps a list of deployable targets; the CLI shows it as no such thing: /v1/invocations. Check the spelling against elan agents, which lists what is actually running.
that belongs to another tenant
The CLI prints this for every 403, so it is often not about tenants. The service's own reason is one of: a credential issued for one project used on another (“this credential may only act on project …”), or an action that needs a tenant admin — running an agent for one of your users, minting a ticket, deciding an admin approval. Call the route directly, or from the SDK, to read the real message, and see Authentication for what each credential reaches.
“… finished without saying anything”
invoice-triage finished without saying anything. That is the agent's own behaviour rather than a timeout — check its logs, and check it returns content from `build()`.
The run ended; this is not a timeout. The agent completed without producing any text — a workflow whose last node is a function returning data rather than content is the usual cause. Read what it did with elan logs --workload <name> --project <slug>, and see Building a workflow.
“… is PAUSED, not finished”
event-planner is PAUSED, not finished. invocation 3f9c2a7e5b1d4c8a9e6f0b2d7a4c1e8f waiting on a node is waiting for a VALUE — …
A third outcome, not a failure. The run stopped to ask something, and a pause can outlast any command, so elan invoke cannot answer it — there is no resume command. What answers it depends on what it is waiting on:
- A value for a workflow node —
elan.runtime.resume(id, approved=True, payload={…}), orPOST /v1/invocations/{id}/resume. - Approval of a tool call — the console's approvals, or
elan.actions.decide_batch(…). - A result your backend owes —
elan.tools.submit(call_id, result={…}). - An approval somebody else gives — the CLI calls this “a pause kind this CLI does not know”. Nothing for you to do but wait for them; see Approvals.
If the agent spoke before pausing — a follow-up question is the commonest shape — the command still exits 0. The SDK methods are in Approvals and autonomy; why a paused run never sends a terminal finished is in Sessions and runs.
invoke prints queued and then nothing
The first invocation after a quiet period is slow, and that is not a failure. An agent scales to zero by default, so its first reply waits for a queue poll, a pod to be scheduled, a sandboxed container to start and ADK to import — usually 15–30 seconds, and longer when the cluster has to add a node. A warm pod answers in the time your model takes.
If it is still silent after a few minutes, the run is either genuinely busy (a long tool call), or the pod never started, or the agent died without reporting an error. Three things to know:
--timeoutdoes not bound a silent stream. The deadline is checked after each event prints, so a run that goes quiet blocks the command until you press Ctrl-C.- Reconnecting does not recover it. Nothing buffers events for a socket that is not open, so a new socket shows only what happens next.
- The run's record does.
GET /v1/runs/{invocation_id}, orelan.runs.get(id), answers mid-run with its status and events so far.
A pod that never starts has no logs at all. The usual cause is a secret that was declared and never set.
Your own client gets an invocation id, and no events
One of three things, in order of likelihood:
- You invoked before subscribing. The subscription that carries your events is created when your first socket opens, and events published before it exists are discarded, not queued. Open the socket, send
subscribe, wait forsubscribed, and only then invoke. The SDK'sruntime.runandruntime.watchingdo this for you. - The run was for one of your users. With
end_user_idset, the events are addressed to that user and go to the socket their browser opened with a ticket — never to your backend's. See Charging your users. - The socket belongs to somebody else. An event is delivered only to a socket opened by the identity that owns the run, so watching with a different credential from the one that invoked receives nothing, even for the right session.
CERTIFICATE_VERIFY_FAILED on the socket only
Your own WebSocket client on a python.org build of Python for macOS, which ships no CA bundle. httpx bundles its own roots and websockets uses the system's, so every HTTP call works and the socket alone fails on a perfectly valid certificate. The SDK and the CLI already handle it; in your own code, pass a context built from certifi:
import ssl
import certifi
from websockets.asyncio.client import connect
context = ssl.create_default_context(cafile=certifi.where())
async with connect(url, ssl=context) as socket:
...“The account balance is too low”
This request could not be started because the account balance is too low. Please top up and try again.
The invocation was accepted and the run ended at once with this as its reply: whoever pays could not cover the deposit, so no model call was made. Mid-run, the same text means a top-up was refused and the next call was skipped. Who pays decides the fix:
- One of your users, when the run named an
end_user_id, the agent is priced, and that user has a wallet: credit the wallet —elan wallet credit <user> 5, or the wallets API. - Your tenant, in every other case. That balance is granted by the platform and you cannot top it up yourself;
GET /v1/balanceshows it. Asksupport@openelan.aito credit it.
Two neighbors read differently. “This account is not currently able to make requests” means the user's wallet is frozen or closed — unfreeze it, do not top it up. “Billing is temporarily unavailable” means the ledger could not be reached; retry. And an invocation refused outright with HTTP 402 (“this account is suspended for an unpaid balance”) is your tenant suspended for an unpaid platform bill; settling it restores service automatically. See Billing and the ledger.
“GitHub App is not installed on …” or “Repository not found”
The platform builds from your remote, so it needs permission to clone it, and that permission is a GitHub App installation granted by an organization admin. Without it the deploy fails at build saying the App “is not installed on acme/agents, or the installation does not include that repository”. On a platform with no App configured, the same gap surfaces minutes later as the clone's Repository not found — which reads like a typo in source.repo and is not one.
Check the installation before the URL: is the App installed on the organization, and does the installation include this repository? Nothing in the CLI or the console can grant it on your behalf. elan logs <deployment-id> shows the build output.
“… is not on any remote branch”
6f1c0a9e4b2d is not on any remote branch, and the build clones the remote — so this would fail with `reference is not a tree` after a minute of building. git push if the commit is only here git fetch if you pushed it from somewhere else, so this check can see it
elan deploy refuses before submitting anything, because a commit that exists only on your machine cannot be cloned. Push it. The check reads your local copies of the remote branches, so if you pushed from another machine, git fetch first.
The deploy succeeded and your change is not in it
The code is built from the commit, not from your working tree. elan deploy resolves your branch to a commit and builds that, so an edit you have not committed is simply not there. Commit, push, deploy again.
elan.yaml is the exception: the CLI sends the file as it is on disk, so an uncommitted edit to it is deployed. The deploy also rewrites source.ref in it to the commit it built, which leaves a change in your working tree — commit it with your next one.
“refused — N problem(s)”
refused — 2 problem(s):
targets.invoice-triage.llm.models
the agent asks for model 'gemini-pro-latest', which is not allowed here. …
targets.invoice-triage
invoice-triage changed but its manifest version is still '0.3.0'. …Your elan.yaml and your code disagree, and the deploy stopped before provisioning anything. A refusal is refused, not failed: it is yours to fix, and every problem is listed at once. The usual ones:
- A target declared in
targets:that the repository does not contain, or an agent in the repository that is not declared. - An agent's model that is not in
llm.models. - A tool calling a host that
egressdoes not allow, or an MCP binding with notool_filter. - An agent that changed while its
versiondid not — bump it.
elan validate runs the same check locally and catches all of these except the version, which needs the last deploy to compare against. Run it before you spend a build. The two files and the check are explained in elan.yaml and MANIFEST.
“… did not become healthy”, or an agent that never answers after a good deploy
The commonest cause is a secret declared in elan.yaml and never set, or set under a different name. Nothing checks that a declared secret exists — not elan validate, not the deploy — so the first thing that notices is the node starting your container: the pod sits in CreateContainerConfigError, never starts, and writes no logs.
- An agent with
minReplicasof 1 or more fails the deploy atsync: “… did not become healthy”. - An agent scaled to zero, the default, deploys cleanly — it has no pod to check — and the symptom arrives later, as an invocation that stays
queued.
Compare elan secret ls with every secrets: and llm.keys name in elan.yaml, set what is missing, and redeploy. See Secrets. A deploy that failed after provisioning says so — “Those resources still exist and are still billed” — and the next successful deploy reuses them.
The first deploy takes much longer than the next
It provisions the project: databases and their roles, a storage bucket, topics and subscriptions. Every later deploy reuses all of it and builds only the image. elan deploy prints each phase as it starts, elan status <deployment-id> has the phase it reached and why it stopped, and elan logs <deployment-id> has the build output — the same record the deployments API serves. Every command and flag is in the CLI reference.