Get started
Call it from your code
Your backend authenticates as a service user, not as you. Create one, put its id and secret in the environment, install the SDK, and the agent you deployed answers a function call.
Before you begin
- A deployed agent. The samples call
hello-agentinagents, from the quickstart; use your own names if they differ. - An organization admin. Only an admin can create a service user. If that is not you, ask one to do step 1 and hand you the id and secret.
1. Create a service user
A service user is a machine identity in your organization, issued for one project — the project travels inside every token it gets, so it cannot act on another. In the console, open Projects → your project → Settings → API access → New service user, and name it after the backend that will hold it.
Can write is on by default. Turn it off if this backend only runs agents and reads; leave it on if it will credit your users' wallets or mint tickets so their browsers can watch a run. Running an agent needs no write access.
The secret is shown once. Nothing — not the console, not the platform — can read it back. Lose it and you rotate, which issues a new secret and stops the old one working.
To automate it, the same thing is POST /v1/projects/{project}/service-users, called with a tenant admin's token — in practice, one from a service user that can write:
curl -s -X POST https://api.openelan.ai/v1/projects/agents/service-users \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "content-type: application/json" \
-d '{"name": "agents-backend", "admin": false}'It answers 201 with client_id and client_secret. Posting a name that already exists answers 200 and rotates that identity's secret rather than creating a second one. The authentication API has the rest, including exchanging the pair for a token by hand.
2. Put it in your environment
export ELAN_CLIENT_ID="<client id>" export ELAN_CLIENT_SECRET="<client secret>" export ELAN_PROJECT=agents
ELAN_PROJECT is the project the service user was created for. Leave it empty and a call raises locally rather than reaching a project that is not yours. The SDK exchanges the id and secret for a token itself and renews it before it expires, so your code never sees one.
3. Install the SDK
uv add elan-sdk # or: pip install elan-sdk npm install elan-sdk # or: pnpm add elan-sdk
The Python client needs Python 3.12 or newer and is imported as elan_sdk. The TypeScript client needs Node 22.18 or newer and is an ES module; the samples below use top-level await, so run them as a .mjs or .mts file, or in a package whose package.json sets "type": "module".
4. Ask it something
run() is the one-shot: it opens the event stream, waits until it is subscribed, sends your message, collects this run's events until it ends, and closes the stream. The order is the point — a reply published before anything subscribed is discarded, not queued.
from elan_sdk import Elan
with Elan.from_env() as elan:
reply = elan.runtime.run("hello-agent", "what can you do?")
if reply.ok:
print(reply.text)
elif reply.pause:
print("waiting for", reply.pause.kind, reply.pause.message)
elif reply.timed_out:
print("no answer in time; the run may still be going")
else:
print("failed:", reply.error)A refusal raises; an outcome does not. A bad credential, a project the credential is not for, or an agent that is not deployed raises an error before anything runs. A run that failed, paused or ran out of time comes back as a reply that says so:
| Python | TypeScript | What it tells you |
|---|---|---|
| reply.text | reply.text | Everything the agent said, every part joined. |
| reply.ok | replyOk(reply) | It finished. |
| reply.pause | reply.pause | What it is waiting for — its kind and message — or nothing. A paused run sends no finishing event: the pause is where it stopped. |
| reply.timed_out | timedOut(reply) | The wait ran out — five minutes by default — before the run ended. It may still be going. |
| reply.said_nothing | saidNothing(reply) | It ended without saying anything. Not a timeout. |
| reply.error | reply.error | Why it failed, when it did. |
| reply.session_id | reply.session_id | The conversation it ran in. Pass it back to continue. |
The first call after a quiet period is the slow one — the agent scales to zero, so a pod has to start before it reads its queue.
5. Hold a conversation
Each run() starts a fresh conversation unless you pass it a session id. conversation() keeps one session for several turns, and sends them one at a time — two turns at once on one session can make one of them fail.
import asyncio
from elan_sdk import AsyncElan
async def main() -> None:
async with AsyncElan.from_env() as elan:
async with elan.runtime.conversation("hello-agent") as chat:
first = await chat.say("what can you do?")
print(first.text)
second = await chat.say("tell me more about the first one")
print(second.text)
# Keep this to carry on later, from any process.
print("session", chat.session_id)
asyncio.run(main())In Python a conversation holds one stream open across its turns, so it is an async with on AsyncElan. In TypeScript it is a plain object: each say() opens and closes its own stream, and the turns are queued so they never overlap. To pick a conversation up later — from another process, after a restart — pass the same id back: session_id= in Python, sessionId in TypeScript.
The synchronous client cannot hold a conversation
Elan turns every async method into a blocking one, and hands runtime.conversation() and runtime.watching() back unchanged: they keep a stream open across several awaits, so they work only as async with on AsyncElan. From synchronous code, runtime.run() with a session_id carries a conversation one turn at a time.
Where to go next
- Install and configure — every way to construct a client, sync against async, and the errors each call can raise.
- Runtime — invoking without waiting, watching events yourself, seeding state, and answering a pause.
- The API — the HTTP routes underneath both clients, for a language with no SDK.
- Charging your users — running the agent on behalf of one of your own users, so their wallet pays and their browser watches.
run()deliberately cannot do that: a run for one of your users sends its events to them, not to your stream.