#Core concepts
#Decision
The root object. A decision is a human approval requested at a point in a run. It's keyed by the caller's own ids (engine, externalRunId, stepRef), not by a Ratifia-executed step. It carries the reviewer context, the assignee/approvers, the quorum thresholds, the SLA (timeoutAt), and the verdict. The verdict lives on the decision and nowhere else; it's the single source of truth.
Status flows PENDING → APPROVED | REJECTED | EXPIRED | CANCELLED (cancelled when the caller withdraws it before anyone decides).
#Caller
Whatever asked for the decision. You bring your own. Ratifia tracks the decision; it never runs the work. Three kinds of caller are supported today, recorded on the decision as engine, and they differ only in how they wait for the verdict:
- an agent over the MCP server (
engine: mcp), which blocks onawait_decision; - your own code over the REST API (
engine: api), which polls for the verdict; - a durable workflow through the Inngest adapter (
engine: inngest), where the function parks on the engine's own wait-for-event primitive and Ratifia fires the resume event.
The decision-first model is what makes this clean: Ratifia never pretends to run steps it doesn't own. A claim processed on Inngest, or an agent about to deploy, creates zero Ratifia step rows, just the decision when a human is needed. Ratifia is not a durable execution engine and doesn't try to be, because that space is solved.
#Policy
Who approves, how many must agree, and where they answer. A policy is referenced by a permanent key (policy: { key: 'refunds' }), so your code never hardcodes an approver. It doesn't decide when a human is needed: that rule stays in your code and arrives as required.
Every change to a policy is a new version, and each decision records the version it was created under. Only the approvers in that version can vote on it, so editing a policy never changes who may answer a decision that's already waiting. See Policies.
#The Inbox
The reviewer-first surface, and the dashboard's home. It lists the decisions waiting on you: pending ones you may vote on and have not voted on yet, sorted by SLA urgency, each showing its context. When a decision names approvers, it waits only on them. All pending shows every pending decision you can read, so an admin can check on the ones waiting on other people. A reviewer opens one, sees what they're deciding (the AI output, the fields, the diff), and approves or rejects, which resolves the decision and releases the caller. Because the caller owns execution, there are no run/step/DAG views to wade through; Ratifia leads with the decision, not the machinery.
#The app is the floor: notifications and surfaces build on it
Hold one model in your head and the rest follows. There are three layers, and only the first is required:
- The app (the Inbox) is the floor. With nothing configured, an approver logs into Ratifia, sees the decisions waiting on them, each with full context, and approves or rejects. This always works and needs zero setup. Every approval can happen here; it's the baseline the rest extends.
- Notifications reach the person. A notification doesn't change where you approve. It gets the right person's attention that something is waiting, and often that it's time-sensitive: "you have an approval, come act on it." Notifying, not deciding. Each approver controls how they're reached, from their own settings. With nothing configured the decision still sits safely in the Inbox; a notification just means nobody has to go looking for it.
- Quick answers happen where you're notified. A plain yes/no gets Approve and Reject buttons right in the email, Slack or Discord message, for when logging in isn't convenient. Anything more (a choice, a typed answer, a draft to edit) needs the full context, so those messages link back to the app. The one exception: Slack can approve a draft it shows in full.
In short: Inbox = always · Notification = get told · Yes/no = answer where you are. The app is the one thing that's always on.
#How the approver is reached
A policy names who decides. Everyone it names is notified at once, by email, in Slack or Discord when your org has connected them, and by push if they use the mobile app. How each person is reached is their own setting. See Notifications.
Whatever the notification, the decision still waits in the Inbox until it's decided, so a missed or misconfigured notification delays a decision but never loses one. Delivery is durable and idempotent through the outbox, SLA reminders re-nudge before the deadline, and if a decision is still unanswered at timeoutAt the SLA sweeper auto-expires it: the caller sees EXPIRED, i.e. the reject branch, so nothing wedges.
#Where people answer
Yes/no decisions can be answered wherever you're notified. Anything more opens the app.
| The decision is… | Email, Slack, Discord | The Ratifia app |
|---|---|---|
| A plain yes/no | Approve and Reject buttons | ✓ |
| A yes/no with a draft | Slack: Approve and Reject, when it shows the whole draft. Email, Discord: a link to the app | ✓ (and the only place to edit it) |
| A choice or a typed answer | A link to the app | ✓ (the only place) |
| Approved by signing a document | A link to the app | Shows the envelope; only the signature decides |
Nothing to configure: every channel your org has connected shows every decision. A vote from a place that can't decide it (a Slack button on a choice, an email link on a signature decision) is refused and isn't counted.
Signed documents are the exception. A policy can require that every decision under it is approved by signing a document, such as a DocuSign envelope the request attaches. Then the signature is the approval, with legal weight, and nothing else can decide it. See Requires a signed document.
Only the people a decision names can answer it, and each person counts once, wherever they answer.