Documentation
Everything here is done in the browser. Follow the five numbered steps and you will have a working assistant that checks with you before it does anything you would rather approve first.
Run it
You need Docker. That is the whole list — and if you do not have it, the command below offers to install it.
macOS and Linux
curl -fsSL https://heddled.com/install.sh | sh
Windows, in PowerShell:
irm https://heddled.com/install.ps1 | iex
The script checks for Docker, downloads Heddled into a heddled
folder, starts it, and waits until it answers before telling you it is ready.
It asks before installing anything and will not write over a folder that is
already there. Reading a script before piping it to a shell is a good habit:
both are plain files, so curl -fsSL https://heddled.com/install.sh
on its own prints it.
Or do it by hand
Nothing above is magic. It comes down to:
git clone https://github.com/heddled/heddled cd heddled docker compose up
Set HEDDLED_PORT first if 5005 is taken. The install script
takes the same setting.
That is the console, the API, the record of everything, and the background
worker, together. Open http://localhost:5005.
The first time, it asks you to claim it — pick a username and password. That account is the administrator. Do this immediately if the machine is reachable by anyone else: an unclaimed Heddled lets whoever arrives first become the administrator.
You do not need an account with an AI provider to follow this guide. The built-in stand-in works offline and is enough to see a whole conversation, an approval and a full record.
1 · Give it something to do
An assistant on its own can only talk. To be useful it needs actions — one thing each: look a customer up, check a list, send a message. Actions are shared, so the same one can be given to several assistants.
Go to Tools → New tool and pick the kind you need. Most need no code at all.
Pick Look something up and you get a form: a name, what it does, what it needs to be told, and the list to search. The description matters more than it looks — it is what the assistant reads when deciding whether this is the right action for the question in front of it.
Check it works before anything depends on it
Every action has a Test tab that runs it on its own — no assistant, no AI, no conversation. Put in some arguments, press Run, and see exactly what comes back.
2 · Make the assistant
Agents → New agent. Five short steps:
- A name — lowercase, no spaces. It can be changed later, and everything pointing at it follows.
- How it should behave — plain language, as if briefing a new colleague. Say what it is for, what to always do, and what never to do.
- What it may use — tick the actions from step 1.
- What needs your approval — only offers what you ticked above.
- Which model — leave the stand-in for now if you have no key.
Afterwards, everything is on the assistant's own page. What it can do shows what is chosen as removable labels, with the full list folded behind a search — useful once you have more than a handful.
3 · Try it
Press Try it and say something. The panel beside the chat fills in as it works — the question arriving, each action it runs, and the reply going back.
If the assistant ignores an action you expected it to use, the usual cause is the action's description, not the assistant's instructions. Make the description say plainly what question it answers.
4 · Read what it did
Open any conversation under Activity. What happened is the plain-words account: who asked, what was looked up, what came back, what was said. That is usually all you need.
Every step beside it has the underlying record — the exact context the model was sent, every argument, every result, timings and token counts. Press j and k to move between steps, Enter to expand one.
Two other things live on this page: Step in adds a note to a running conversation as though the assistant had been told it, and Save as a test keeps the conversation to replay later — see Save a test.
5 · Make it check with you
On the assistant's page, under What it checks with you first, add a rule: choose the action, tick ask me first, and optionally cap what it may spend in a day or hide sensitive fields from the record.
Now the assistant stops before that action and waits. The platform holds the turn — this is not an instruction the model could talk itself out of.
The request also goes wherever you set up under Settings → Where things go: a webhook, Slack, or just the console. Whoever approves gets a link that needs no account.
Let it start itself
Under When it acts on its own, three things can begin a conversation with nobody typing:
| Set off by | You give it | Good for |
|---|---|---|
| A time of day or week | How often, and at what time | The 08:00 summary of what is overdue |
| A file arriving in a folder | The folder to watch | A scanner, an export, a colleague saving a file |
| An email arriving in a mailbox | Mail server, sign-in, folder | An invoices@ or support@ address |
Heddled remembers where it got to, so nothing is handled twice — even across a restart. Mail server details are kept under Settings rather than in the assistant's file, because that file is meant to be shareable and a password is not.
Let other systems in
Every assistant has its own addresses, listed on its page, ready to copy. Whether each is open depends on what the assistant is mounted on.
A program signs in as Bearer <username>:<password>,
or with an integration key for a system that has no account. Whatever arrives
this way obeys the same rules — your approval gates still apply.
curl -X POST http://localhost:5005/api/agents/billing_support/webhook \
-H "authorization: Bearer alex:your-password" \
-H "content-type: application/json" \
-d '{"text": "Is invoice F-2231 paid?", "sync": true}'
It works in the other direction too: an assistant can use any MCP server as a source of actions, and can be given another assistant as an action so a specialist handles part of the work.
Save a test
Have a conversation that goes the way you want, then press Save as a test on it. Later, after you have changed the assistant, replay it and see what changed.
A replay sends the same messages to the current version, with actions in playback: each gives back exactly what it gave back the first time. Running your tests can never send a real email or move real money.
Heddled reports whether the assistant used the same actions with equivalent arguments, and whether the answer still passes its checks — exact match, contains, a pattern, or judged by a model.
Publish a version
Every time you change an assistant it gets a new version, and every version is kept. Publish binds one of them to an environment, and that decides what actually runs:
| Environment | Runs |
|---|---|
dev | Whatever you are editing right now |
staging, prod | The version published there — it keeps running while you edit |
Publishing to prod asks for a passing test run on that exact version first. You can overrule it, but it will ask. On the assistant's page, Earlier versions lists every definition it has had — compare any of them with what you are editing now, or put one back.
Work arriving from outside belongs to dev until you say
otherwise, so publishing changes nothing at first. Set default_env
to prod under Settings once you are publishing deliberately —
then editing an assistant stops changing what your live traffic does.
Add people
People is where accounts live. Three levels: an admin can change everything including settings and people, a member can build and run assistants, and a viewer can look but not change.
Somebody suspended keeps their record but cannot sign in. If you lock yourself
out, heddled user add on the machine itself is the way back in.
Where it all lives
Each assistant is one file, and the console edits that same file. Neither is the "real" version — they are the same bytes, so you can review changes, keep them in version control, or edit by hand.
# agents/billing_support.yaml name: billing_support description: Invoice and payment questions for the finance team. model: anthropic/claude-sonnet-4-6 instructions: ./billing_support.md # or write them inline adapters: channels: [webchat, webhook] tools: - lookup_invoice - issue_refund - agent:office_helper # another assistant, as an action triggers: - schedule: "0 8 * * 1-5" message: "Flag anything unpaid." policies: - tool: issue_refund requires_approval: true budget: { max_eur_per_day: 500 } - tool: "*" redact: [iban, creditcard] # kept out of the record memory: session: auto # rolling summary, on by default expose: mcp: true # other systems can call it
Actions live in tools/<name>/ — a manifest saying what goes
in and out, and a handler if it needs one:
# tools/lookup_invoice/handler.py def handle(args, ctx): invoice = db.find(args["invoice_number"]) return {"status": invoice.status, "amount_eur": invoice.total}
Settings and secrets are kept separately, in the store rather than in files — those are the things you would not want to commit.
Models
An agent names its model as service/model. Each service has its
own key, so you can have several configured at once.
| Written as | Service | Key |
|---|---|---|
anthropic/claude-sonnet-4-6 | Anthropic | ANTHROPIC_API_KEY |
openai/gpt-4o | OpenAI | OPENAI_API_KEY |
deepseek/deepseek-chat | DeepSeek | DEEPSEEK_API_KEY |
groq/llama-3.3-70b-versatile | Groq | GROQ_API_KEY |
mistral/mistral-large-latest | Mistral | MISTRAL_API_KEY |
openrouter/… | OpenRouter | OPENROUTER_API_KEY |
ollama/llama3.2 | Ollama, on your machine | none needed |
vllm/your-model | Your own server | none needed |
mock/echo | Built-in stand-in | none needed |
Keys go under Settings or in the environment. Any service
that speaks the OpenAI chat-completions API works — point
<service>_base_url at a proxy or a gateway if you need to.
Command line
| Command | What it does |
|---|---|
heddled dev | Console with the trace pane, opened on the Test tab |
heddled serve | Console, API and worker together |
heddled worker | Run turns as a separate process |
heddled chat <agent> "…" | One scripted turn |
heddled trace <session> | Print a conversation, event by event |
heddled new agent|tool|policy | Scaffold one; --from clones an existing one |
heddled mv agent|tool <old> <new> | Rename, following every reference |
heddled rm agent|tool <name> | Delete, refusing if something depends on it |
heddled tool test <name> | Run a tool on its own |
heddled approve [id] | List or resolve approvals |
heddled eval run <agent> | Replay saved conversations |
heddled deploy <agent> <env> | Publish a version |
heddled user add | Add somebody, from the machine itself |
Configuration
| Variable | Purpose |
|---|---|
HEDDLED_ROOT | Project root — agents, tools, data, var |
HEDDLED_PORT / HEDDLED_HOST | Where the console listens |
HEDDLED_DEFAULT_ENV | Environment for work arriving from outside |
HEDDLED_HTTPS | Set behind TLS so session cookies are marked secure |
HEDDLED_KEEP_FULL_CONTEXT_DAYS | How long full model contexts are kept (default 90) |
HEDDLED_WEB_ONLY | Serve HTTP only; run heddled worker separately |
OTEL_EXPORTER_OTLP_ENDPOINT | Export every turn to your own tracing stack |
Anything set in Settings wins over the environment, so you can change most things without a restart.
Before you put it on a network: claim the console
immediately — an unclaimed Heddled lets whoever finds it first make themselves
administrator. Put it behind TLS and set HEDDLED_HTTPS=1.