A side effect is claimed before it is caused
The idempotency claim is written and committed before the handler runs. A claim written afterwards proves nothing about a process that died in between — which is the exact case it exists for.
Triggers, conditions, and runs that survive the things that kill a run. Mail arrives, a time comes round, another automation finishes — and the engine knows the difference between failing and correctly declining to act.
An automation is not a scheduled prompt. A prompt runner is one function call: a crash anywhere inside it loses everything and there is nothing to resume. This is a pipeline with durable state at every joint.
connector / scheduler / agent / api
│ emits
▼
core/events.py Event(id, kind, source, subject, data, trust)
│ ── deduplicated on (source, external_id)
▼
automation/router.py which automations care?
│
▼
automation/triggers.py a registry, not an if/elif chain
│
▼
automation/conditions.py deterministic first, semantic only if needed
│
▼
automation/executor.py the driver, resumable from any state
│
┌────────┴──────────────────────────────────────┐
│ plan → approve → act → verify → retry → done │
└───────────────────────────────────────────────┘ ┌──────────────────────────── CANCELLED (user)
│
PENDING ──► RUNNING ──► WAITING_FOR_APPROVAL ──► EXECUTING ──► VERIFYING ──► COMPLETED
│ │ │ │
│ └────────────► BLOCKED │ │
│ (condition false, permission denied) │
│ ▼ ▼
└──────────────────────────────► RETRYING ──► ESCALATED / FAILEDThe idempotency claim is written and committed before the handler runs. A claim written afterwards proves nothing about a process that died in between — which is the exact case it exists for.
That means “we tried and do not know”. The retry verifies what happened rather than repeating it.
An action with a verifier that did not pass is not complete. An action with no verifier must say why — “nobody wrote a checker” and “there is no way to check this” are different answers, and reporting them identically lets the first become permanent.
Wall clock, actions, retries and model iterations all have ceilings. Exceeding one is BLOCKED, not an exception.
An event produced by automation A's action carries A's run id. A depth limit and a self-trigger check make the A → event → A loop terminate.
An approval can take hours. Counting that against the run killed every run that waited.
The trigger context used to be pasted into the prompt unfenced — "NEW EMAIL(S) that just arrived:\n" + …. That is the injection surface, and the fence that closes it already existed one directory away, written for the browser.
| and one more | because |
|---|---|
| There is one permission system, and it is not in here | Automation calls the same gate an interactive agent calls. It may never widen what you already allow. |
| A semantic verdict is data, never authorization | A condition returns a bool and a confidence. The gate reads neither. |
| Unknown actions fail closed | An automation is the caller most likely to meet an action name a model invented. |
| Nothing accepts an unsigned delivery from a provider that signs | A provider whose secret is unset is refused with 503 rather than degrading to unauthenticated. |
Free while it is in beta. Bring your own model — or run a local one and never leave the machine.
Join the waitlist→