Open the console

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-agent in agents, 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.

Request
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:

PythonTypeScriptWhat it tells you
reply.textreply.textEverything the agent said, every part joined.
reply.okreplyOk(reply)It finished.
reply.pausereply.pauseWhat 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_outtimedOut(reply)The wait ran out — five minutes by default — before the run ended. It may still be going.
reply.said_nothingsaidNothing(reply)It ended without saying anything. Not a timeout.
reply.errorreply.errorWhy it failed, when it did.
reply.session_idreply.session_idThe 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.

Request
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.

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