What's happening underneath
You do not need this page to use Kelam — describing what you want is the supported path, and everything below gets written for you. It's here for when you want to know what the thing you asked for actually became.
An agent
An agent is a folder of plain files. Four kinds, all optional except the first:
| agent.yaml | Its voice, its languages, which models run the call, and which built-in capabilities it's allowed to use. Full field list → |
| scenarios/*.md | What it does and how it behaves, written in English. Every file is concatenated into the system prompt, in filename order. |
| tools/*.py | Things it can actually do mid-call — check a calendar, look up an order, hit your API. Ordinary Python functions. |
| tests/*.yaml | Assertions you can run before shipping: "greets and stays on topic", "never invents an appointment". |
The Builder writes these for you; kelam pull puts them on your disk. Same
files either way — an agent built in the browser can be pulled, edited in an editor, and
pushed back.
A workspace and its number
Your workspace is your account: your agents, your calls, your secrets, your people. It comes with one phone number, shared by every agent on it. The first agent that wants a phone claims it and answers incoming calls; every agent after that uses the same number to dial out. Nothing to buy, nothing to point at anything.
Two escape hatches. An agent that never needs a phone at all
(kelam create x --no-number) lives only in the browser and costs nothing to
run. And an agent that needs a line of its own can buy a dedicated number
(--buy-number) instead of sharing — a paid option.
Several agents on one number is the intended shape, not a workaround: one front desk answers, and hands the live caller to whichever colleague should take it. See the switchboard example.
Scenarios — the instructions
A scenario is a markdown file describing the agent's job. Write it the way you'd brief a new hire: who calls, what they want, what a good outcome looks like, what to do when it goes sideways.
# scenarios/01-instructions.md
You are the front desk for Brightside Dental. You answer the main line.
- Greet warmly, then find out what they need in one question.
- To book: get the patient's name, the reason, and a day. Offer the two
nearest open slots. Read the booking back before you confirm it.
- If they describe severe pain or bleeding, treat it as urgent and offer
to transfer them to the on-call dentist.
- Never quote a price. Say the office will confirm costs on arrival.
Rules of thumb: short sentences, concrete outcomes, and say what to do when it fails.
Don't write placeholder brackets like [patient name] — the agent will read
them aloud on a live call. Facts that change (your hours, a booking link) belong in
context, not in the prompt.
Tools — what it can do besides talk
Two kinds. Built-in capabilities are one line each in agent.yaml:
listing it is what turns it on, there's no second switch.
| end_call | Hang up deliberately when the conversation is done. |
| transfer | Send the caller to a real phone number, off-platform. |
| handoff | Hand the live call to another one of your agents — the caller never leaves the line. With any_agent it can route to any colleague. |
| owner_callback | Dial the account owner into the call as a third voice. |
| dtmf | Press keypad digits — how it gets through "press 2 for billing". On by default. |
| hold | Take a "can you hold a second" gracefully instead of talking into the void. |
| fetch_context | Re-read the stored facts mid-call, so it sees anything written since the call started. |
| background_noise | Change the ambient room behind its voice mid-call. |
| ask_operator | Hand a piece of back-office work to your workspace's operator during the call. |
Every agent also gets, for free, the ability to read its own past: its recent text threads and its previous calls with the person on the line.
Your own tools are Python files. A function with the @tool decorator
becomes something the agent can call in conversation; its docstring is what the model reads
to decide when to call it.
# tools/check_calendar.py from kelam import tool, secret, context import httpx @tool async def open_slots(day: str) -> str: """Open appointment slots for a given day, e.g. 'Saturday'.""" r = await httpx.AsyncClient().get( context("BOOKING_API") + "/slots", params={"day": day}, headers={"authorization": "Bearer " + secret("BOOKING_KEY")}, ) return r.text
Keep them fast — a tool call happens inside a conversational turn, so anything slow reads as dead air. The tool contract in full →
Context and secrets — what it already knows
Context is facts an agent has before the phone rings, so it never has to ask for something you already know. Three lanes, narrowing:
| workspace | True for every agent you own: your business hours, your address, your tone. |
| agent | True for one agent: which price list it quotes, which calendar it books. |
| caller | True about one person: their name, their last order, what they asked for in March. |
All three are fetched and seeded into the agent's head before every call connects. The
caller lane is normally filled in automatically after calls, which is why an agent that has
spoken to someone before opens the second call already knowing who they are. You can edit
any lane by hand — in the Builder's Memory tab, or with kelam context — and a
one-off fact for a single call rides along with -C key=value.
Secrets are the same idea for things that must stay hidden: API keys, tokens. They
are stored encrypted, never printed back, and resolve only inside tool code via
secret("NAME"). Set one with kelam secret-set; the value never
appears on your command line.
A call
Four kinds, one record type:
- Inbound — somebody dials your number and the agent that owns it answers.
- Outbound — you (or Claude, or a script) tell an agent to call a number. It listens first to work out whether it got a person, a voicemail box or a phone tree, then acts accordingly.
- Browser — a real conversation over WebRTC on the agent's page. No telephony, no number, and the fastest way to try a change.
- Agent to agent — two of your agents talking directly to each other. No phone network at all, so it costs nothing; this is how automated voice tests run.
Whatever the kind, when it ends you get: a recording, a word-for-word transcript, a one-line summary, who hung up and why, whether a real human actually answered, how long it took, how many turns it ran, and exactly what it cost.
You can follow a call while it happens — the transcript streams — and you can listen in silently, invisible to both sides. And you can end one at any moment.
Extraction — answers as data
Most calls are placed to find something out. Instead of reading transcripts, ask for typed fields up front:
$ kelam call supplier-bot +12065550123 \ -e "price:number:the quoted price per pallet in USD" \ -e "in_stock:boolean:whether they have it in stock" \ -e "lead_time:string:how long delivery takes"
The agent is steered to find each one out naturally — it never announces that it's collecting fields — and after the call a pass over the transcript lands the values on the call record. Twenty calls become a table. You can also run extraction over calls you already made, with a field you only thought of afterwards.
Versions and deploys
Every save is a new version. Nothing is edited in place. Deploying is what makes a version the one that answers the phone; older versions stay callable, which is what makes the next section possible.
Practically: push creates the version, deploy makes it live. In
the Builder both happen when you accept a change. Every call records the exact version that
took it, so "which config said that?" is always answerable — and you can replay a bad call
against that exact version.
Tests and experiments
Two ways to know a change helped.
Tests live in tests/*.yaml and run against the deployed agent.
A turns test is text-only and free — it feeds the agent lines and asserts on how it
replies and which tools it fires, including the ones that read history. A voice
test is a real agent-to-agent call with a scripted caller persona, which costs nothing
because no phone network is involved. Test calls are hidden from your stats.
Experiments compare variants of one agent on real traffic — a different voice, a cheaper model, a rewritten prompt — split by weight, sticky per caller, scored on a metric you choose (completion, cost, latency, a human thumbs-up, or one of your extracted fields). It won't declare a winner before it has enough calls to mean anything.
What it costs
Cents a minute, billed to the second: a small connect fee plus per-minute talk time, capped per call. A call nobody answers is free. Browser calls and agent-to-agent calls have no telephony cost at all, which is why the Builder encourages you to test that way.
Every call stores its own exact provider usage and a cost breakdown, so
kelam costs and the Usage tab report real money spent — never an estimate.