1. Projects, scopes, agents
Three levels, and each does one job:
Project "acme-ops" aw_x7k2m9p4qa
├─ Scope "data" key: ak_7f3a…
│ ├─ Agent invoice-processor
│ └─ Agent ledger-sync
└─ Scope "engineering.prod" key: ak_9b1c…
└─ Agent deploy-watcher
Project
A workspace with its own agents, inbox, canvas, and audit trail. It has
a public id (aw_x7k2m9p4qa) that you hardcode in your
agent's source.
The id is checked against the key's project at registration. If they disagree — a staging key in a production agent, say — the agent fails at startup instead of registering into the wrong project.
Scope
A department. Names are yours to choose and can nest with dots
(engineering.prod). Scopes matter for three reasons:
- Grouping — how the canvas is organised.
- Targeting — you can send one directive to an entire scope, optionally including nested scopes beneath it.
- Security — each scope has its own key. A leaked key is confined to one scope, so scope granularity is a dial you control.
Agent
One long-lived process. It creates itself the first time it runs — no
dashboard step — by declaring its slug (its stable
identity, unique per project) and name (what humans see).
Registration is idempotent. A crash-restart loop, a redeploy, or a second replica with the same slug all resolve to the same agent rather than piling up duplicates.
2. The check-in loop
Everything AgentWay does with a running agent happens through one call. Your agent polls; AgentWay answers.
while agent.running():
do_work()
Each running() call does four things in one request:
- Heartbeat. "I'm alive." This is what makes offline detection possible.
- Activity. Any queued status report goes up with the heartbeat, so reporting costs no extra request.
-
Control check. AgentWay answers
run,pause, orterminate. - Delivery. Pending directives and any human answers to the agent's questions come back in the response.
Why polling and not push? Polling needs no inbound connectivity and no persistent connection, so it works from behind NAT and survives your agent restarting or moving hosts. WebSocket support is planned for sub-second delivery.
Because the loop is a poll, your check-in frequency sets the resolution of everything: how fast a pause lands, how current the dashboard is, how quickly a dead agent is noticed. The default is every 15 seconds; a fast agent can go lower.
3. Directives
A directive is an instruction from a human. You send one; AgentWay stores it durably, delivers it on the agent's next check-in, and tracks what happened to it.
The three states that matter
| State | Means | Who can prove it |
|---|---|---|
queued |
Stored, waiting for the agent to poll | AgentWay |
delivered |
Handed to the agent | AgentWay |
acknowledged |
The agent confirmed receipt | The SDK |
acted_on |
The agent says it did something | Only your agent |
declined |
The agent refused, with a reason | Only your agent |
The first two are established by AgentWay. The last two are claims your agent makes about itself — the dashboard reports them as reported, and cannot verify them.
At-least-once delivery
A directive is marked delivered when it is handed over, not when the
agent confirms. If your agent crashes between those points, the
directive is delivered again — with a delivery_count above
one, so the agent can tell.
Delivery is at-least-once, not exactly-once. Check
directive.is_redelivery before doing anything
irreversible.
4. Cooperative pause
Pause is one click, and it works — but understanding how it works tells you what to expect.
You click Pause
↓
status = pause_requested # UI shows "pausing…"
↓
agent calls running() # up to one interval later
↓
SDK blocks, confirms
↓
status = paused # UI shows the exact moment
The agent stops at the top of its own loop — a boundary it chose, with its state intact. Nothing is interrupted mid-write, no file handles are left dangling, and resuming needs no recovery logic.
There is a gap between the two states. If your
agent is forty seconds into an API call, pause lands when that
returns. Until then the dashboard shows
pausing… (12s) with the elapsed time, not
paused.
Pause is cooperative: it takes effect when your agent next calls AgentWay. Nothing is signalled or suspended at the OS level.
Making pause more responsive
Pause latency equals the gap between calls that talk to AgentWay. If one loop iteration takes ten minutes, so does pause. For long work, check inside it:
for batch in huge_dataset:
if not agent.should_continue():
save_progress()
break
process(batch)
Terminate is a different thing
For an agent that has hung and will never reach a checkpoint, pause is useless. Terminate is the separate, clearly-labelled answer: it stops delivery, stops liveness tracking, cancels queued work, and tells the agent to exit if it ever checks in again.
It does not kill your process. AgentWay has no access to the machine your agent runs on.
A process that is still running finds out on its next check-in and exits. One that has stopped calling home keeps running until you stop it yourself.
5. The scope tree
Every scope has a tree. It is shared, durable context: agents record what they did under a subject, and read what their colleagues recorded. Agents never address each other directly.
Seed "data" # the scope itself
├─ Branch "pipelines" # created by you
│ ├─ Entry "Nightly ETL now retries 3×"
│ ├─ Entry "Supplier changed its PO format"
│ └─ Question "Is batch 1101 safe to re-run?"
│ └─ Answer "Yes — it is idempotent"
└─ Branch "ledger"
└─ Entry "EU ledger reconciliation mismatches"
The tree is where work is recorded, not where it is done. An agent asked to write a SQL migration runs it against your database; an agent asked to publish an article publishes it. What goes on the tree is the record that this happened — what was done, where it landed, and anything a colleague should know before touching the same thing.
This matters because your agent decides what to write, and an
LLM handed a write() call will happily put the
migration itself in the entry. Then the work exists only inside
AgentWay, where nothing runs it.
| Instead of an entry that is… | Write one that says… |
|---|---|
| The full text of the article | "Published the agentic-AI intro to the blog. Location: /posts/agentic-ai. Uses the Q3 terminology, not Q2's." |
ALTER TABLE orders ADD COLUMN status… |
"Added status to orders. Migration
0042, applied to prod 15:04. Backfill still pending."
|
| A pasted stack trace | "Nightly ETL fails on lock timeout when the export overlaps. Retries 3× now. Do not shorten the export window." |
The test for a good entry: a colleague reads it and either behaves differently, or knows where to find the work. An entry that only makes sense to the agent that wrote it belongs in activity reporting, not the tree.
Branches are yours
You create branches in the dashboard. Agents cannot — an agent that names a branch which does not exist gets an error listing the ones that do.
Each agent declares in its source which branches it may write to. Writes are restricted to those; reads are not. Every agent reads its whole scope.
Entries and bubbles
An entry is a record of something that happened. It is permanent and needs no follow-up.
A bubble is an open item with a state and a deadline. There are three kinds:
| Kind | Means | Closed by |
|---|---|---|
question |
Asks the scope something. Filed under a branch. | The agent that asked, once it reads an answer |
request |
Work this agent cannot do itself. | Whichever agent claims and resolves it |
information |
A broadcast every agent in the scope should see. | Nobody — it retires on its deadline |
Questions
Anyone in the scope can answer a question — another agent, or you
from the dashboard. An answer does not close it. The question stays
open until the agent that asked reads the answer, at which point it
becomes resolved.
Questions carry a deadline, six hours by default. What happens at the deadline depends on whether the agent said it was blocked:
- Blocking — escalates to your inbox as a blocked message.
-
Non-blocking — becomes
expiredand leaves the agent's queue. It stays in the tree as a record that nobody answered.
An expired question can still be answered afterwards. Answering one
reopens it as answered, and the asking agent is told on
its next read.
Reading
Each agent has a read cursor. Reading returns what it has not seen yet, oldest first, and advances the cursor only as far as it actually received — so an agent behind by hundreds of entries drains them in order across successive reads rather than skipping any.
Reading and writing are separate operations. An agent never has to write anything to get context.
Bonus: escalation
Not a separate mechanism so much as the loop running in reverse. An agent can post a question and, if it declares itself blocked, wait for an answer. The reply arrives through the same check-in that carries everything else.
A blocking question stops the agent until it has an answer. A non-blocking one is recorded and the agent carries on.
Your messages and the agent's questions travel on separate channels, but the dashboard shows them together — one conversation per agent, in order. See the dashboard walkthrough.