# Under the hood

> One persisted object model - models, tasks, transitions and nodes - executed by two runtimes, which is why a three-day scenario and a service restart are the same kind of non-event

Source: https://aipril.dev/docs/processes/under-the-hood

## One model, and all of it written down

Everything in this block is one object model, and every part of it is persisted. Nothing that
matters is held in memory.

- A **model** is the definition of a process: its task types and their fields, its statuses, the
  transitions between them, the conditions on those transitions, and the roles. Publishing a model
  produces a version.
- A **task** is an instance of a task type. It carries field values, participants, a status, a place
  in a tree - parent, children, root - and a history of every change made to it.
- A **transition** is a described move between statuses. When it is gated by an approval, a timer or
  a piece of work, the attempt at it is itself a record rather than a moment in flight.
- A **scenario** is a definition that compiles into **nodes** - the executable steps. Each node's
  state, its result, and when the next one is due are rows.

Two runtimes execute that model. The **process service** owns where the work stands: tasks,
statuses, transitions, roles, approvals. The **orchestrator** owns what runs between the moves:
scenarios that call things, wait, branch, retry and compensate.

They exchange events and results across defined seams, and because both sides of every seam is a
row, a failure there is a delayed row rather than a lost one. That is the same reason a scenario can
take three days and a restart on the second day changes nothing: the state was never anywhere else.

```mermaid
flowchart TD
    U["A person files,<br/>approves, moves a task"] --> BPM["Process service<br/>tasks, statuses, transitions, roles"]
    BPM -->|"status changed,<br/>task created"| ORCH["Durable orchestrator<br/>steps, waits, retries, compensation"]
    ORCH -->|"result written into<br/>the task's fields"| BPM
    ORCH --> CONN["Connectors<br/>mail, messengers, HTTP,<br/>the platform's own services"]
    BPM --> ENG["Query engine<br/>tasks as a live model"]
```

## The process service

It is written in **Java on Spring**, and the choice follows from what this component actually does:
hold transactional state that many people change at once, and be exactly right about it.

- **Moving a task is not one write.** It is a status change, a history entry, participants
  resolved, guards evaluated and an event queued - and either all of that happens or none of it
  does. A mature transaction manager is the tool for that shape of work, not a convenience.
- **Two people press the button at the same time.** Contention on the same task is the normal case,
  not the edge case, so the service locks the row deliberately instead of hoping. This is the
  ecosystem where that has been ordinary practice for two decades.
- **Scheduled work has to run once across replicas.** Timer sweeps and delivery loops run with a
  distributed lock, so raising a second instance adds capacity rather than duplicate work.

**Isolation is by schema, and it sits under the data layer.** The connection itself is routed per
organisation from the identity in the request, so a query in a process is physically scoped instead
of filtered, and isolation is not something every query has to remember to do. The routing comes
from the caller's own token, which is also why service credentials are not accepted as a substitute
for a user's identity on the paths where per-model access is decided.

**Guards are structured, not code.** A transition's condition is a stored predicate evaluated by the
service, not an expression language nobody can review. Field validation is separate from it, applied
as unary tests on the value. Both are inspectable, which matters when the question is "why did this
request take that branch".

**Roles resolve at the moment they are needed.** Participants are held at tenant level rather than
per process model, and delegation rides the same resolution layer that resolves roles - so a
stand-in is a resolution result, not a copy of an assignment. Fields flagged as naming the assignee
or a watcher bridge into participants automatically.

**Field-level access is opt-in per process.** Deny-by-default grants in both directions - a write
requires an explicit edit grant per field, and a read omits values the caller has no grant for,
including from the bodies returned by writes and transitions. Off by default because most processes
do not need it; on, it is strict, not advisory.

**Nothing is hard-deleted.** A removed task is deactivated, keeps its history, and emits an event;
reads filter it out.

## The durable orchestrator

**The database row is the source of truth.** Workflow state is persisted at every step instead of
held in memory, so a run survives a restart of the service executing it. When a step is due to fire
is a column, and the fast dispatch path is an optimisation on top of it - a reconciler sweeps for
anything the fast path missed, which is what makes "fires exactly once" true rather than usually
true.

```mermaid
flowchart TD
    DEF["Scenario definition"] -->|"compiled"| NODES["Nodes<br/>each with its own persisted state"]
    NODES --> RT["One trusted runtime"]
    RT -->|"built-in node"| SVC["Platform services<br/>engine, spreadsheets, ledger"]
    RT -->|"agent node"| AI["AI agent<br/>metered, bounded, recorded"]
    AI --> REV["Human step<br/>a person reviews the result"]
    REV -->|"accepted"| RT
    REV -->|"rejected"| ERR["Error path,<br/>or compensation"]
    RT -->|"declarative node<br/>from an installed package"| EXT["An external HTTP service"]
    EXT -.->|"inbound webhook,<br/>signed token"| RT
    RT -->|"state, result, next due"| DB[("Persisted run state")]
    DB -->|"reconciler sweeps<br/>what the fast path missed"| RT
```

**An agent is a node, and review is a step.** A call to an agent runs on the same durable machinery
as any other node - its result is persisted, its cost is metered, its call is recorded - and where
what it produced is consequential, the next node is a human step: the scenario stops, a person
looks, and it continues or takes the error path. Nothing about that is special-cased for AI, which
is the reason it can be trusted with the same guarantees as everything else on the board.

**A node package cannot contain code.** An installed package declares an outbound call or an
inbound webhook as data, and the same trusted runtime that executes built-in nodes executes that
declaration. So extending the palette is a data change, not a deployment - nothing third-party is
loaded into the process, and there is no sandbox to trust because there is nothing to sandbox.
Anything that genuinely needs code runs on the customer's own infrastructure behind an ordinary
HTTP node.

**The step vocabulary is deliberately small**: call a service, call a connector, ask a human and
wait, wait for a period, branch, set, return, loop, repeat until, run in parallel, and try with
compensation. A saga's compensation is part of the definition, not an afterthought - it is how a
scenario that already changed something outside the platform gets undone when a later step fails.

**Fan-out is bounded at compile time.** The parallelism of a branch or a loop is checked when the
definition is compiled instead of discovered at run time, and the per-iteration item count is
capped at execution. A parent fails atomically if a branch fails: no half-successful fan-out.

**Ingress is authenticated and quota-limited.** Webhook tokens are signed, revocable by rotation,
and rate-limited; cron definitions carry their timezone and are validated when saved, not at the
first missed midnight.

**Background loops re-resolve their tenants each pass**, so an organisation created while the system
is running is picked up on the next sweep, not at the next restart.

**Payloads are encrypted and retained for a bounded time**, and every model call inside a scenario
goes through the same metered transport as everywhere else - there is no cheaper unmonitored path
for an agent step.

## The seams between the runtimes

Three of them, each with a defined failure mode.

- **Events out.** Task created, status changed, participant changed - delivered through an outbox
  rather than an in-process call, so a consumer being down delays delivery instead of failing the
  transition.
- **Actions in.** A transition bound to a scenario holds the task in a processing state and records
  the attempt. The scenario's completion writes back through a single-use resume token - so a
  duplicate completion is applied once - and a sweeper finalises records that outlive their timeout,
  because a task stuck in "processing" forever is the failure people actually complain about.
- **A lost start is reconciled.** A start event that never linked to a run is found by a sweep and
  either re-dispatched or cleaned up, instead of sitting as an orphan nobody looks for.

## Tasks as data

Process data does not have to be exported to be reported on. A published process model is synced to
the [data engine](https://aipril.dev/docs/engine.md) as a live relation, so tasks, statuses and their custom fields
become an ordinary queryable model - date fields included, so grouping by month works on them like
any other date. The sync runs on every application publish that touches a process, not as a manual
step.

That is why a task board and a revenue chart can sit on one dashboard and filter each other: they
are the same kind of thing to the engine. Access to such a model is granted at model level.

## Related

- [Forms and records](https://aipril.dev/docs/processes/forms.md) - what the reader meets on top of this machinery.
- [Automation](https://aipril.dev/docs/processes/automation.md) - the orchestration side, and the step vocabulary in use.
- [Data engine](https://aipril.dev/docs/engine/under-the-hood.md) - the plane a published process model is synced into.
- [Connectors](https://aipril.dev/docs/platform/integrations/connectors.md) - the typed operations a process step calls.
- [AI harness](https://aipril.dev/docs/harness.md) - what an agent step in a process actually runs through.
