FlowplaneDocs

#Core concepts

#Decision

The root object. A decision is a human approval requested at a point in a run. It's keyed by the engine's run — engine, externalRunId, stepRef — not by a Flowplane-executed step. It carries the reviewer context, the assignee/approvers, the quorum thresholds, the SLA (timeoutAt), the route + surface (below), 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.

#Engine

The durable engine that actually executes your workflow steps — you bring your own. Flowplane runs on top of it and tracks only the decisions. Inngest is the default and first-class adapter today; Temporal and Trigger.dev adapters are planned.

The decision-first model is what makes this clean: Flowplane never pretends to run steps it doesn't own. A claim processed on Inngest creates zero Flowplane step rows — just the decision when a human is needed. Flowplane is not a durable execution engine and doesn't try to be — that space is solved. (A built-in "bundled" engine existed early on; it was removed entirely, so an external engine is now required.)

#The Inbox

The reviewer-first surface, and the dashboard's home. It lists decisions awaiting a human, sorted by SLA urgency, each showing its context. 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 resumes the engine. Because the engine owns execution, there are no run/step/DAG views to wade through; Flowplane leads with the decision, not the machinery.

#The app is the floor — routes 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 Flowplane, 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.
  • Routes reach the person. A route 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. Without a route the decision still sits safely in the Inbox; a route just means nobody has to go looking for it.
  • Surfaces let you act without the app. A surface moves the binding approve/reject outside the app — a signed email link, Slack buttons, a DocuSign envelope — for when logging in isn't convenient or possible. It's a convenience layer on top of the floor: some decisions can be settled off-app, but not all (some need the full context, or legal weight), and the Inbox is always there as the fallback.

In short: Inbox = always · Route = get told · Surface = act elsewhere. Routes and surfaces are independent (below) and both optional; the app is the one thing that's always on.

#Route — how we reach the approver

Two things people conflate are actually independent axes, and Flowplane keeps them separate: how you reach the approver (route) and how they approve (surface).

A route is the delivery strategy for getting a decision in front of a human. A route is composed of one or more channels — the individual mechanisms we can deliver through:

  • email (available today, via the durable outbox → Novu/Brevo)
  • Slack DM / channel — roadmap
  • phone push (ntfy) / SMSroadmap
  • webhook to your own app — roadmap

A route can fan out to several channels at once ("Slack and email"), or escalate across them as the SLA deadline nears — e.g. Slack first, then phone if it goes unanswered. Delivery is durable and idempotent through the outbox, so a channel outage never drops the ask, and SLA reminders re-nudge before the deadline. If a decision is still unanswered at timeoutAt, the SLA sweeper auto-expires it (resuming the engine on the reject branch) so a workflow never wedges.

#Surface — how the approver approves

A surface is where the binding verdict is produced — and it's a different axis from the route that announced it. The same decision can be reached one way and approved another:

  • Inbox — the reviewer clicks Approve/Reject in the dashboard (today's default)
  • Email — a signed action-link in the notification itself; approve or reject without logging in (available today)
  • Slack — yes/no buttons in the message itself, for low-ceremony approvals (available today)
  • DocuSign — a signed envelope, when the approval needs legal weight (e.g. large payouts) — roadmap
  • API — a signed call from your own app — roadmap

Whether an approver can bind on a given surface depends on their verification state: inbox and email are zero-config, while slack and docusign must be verified first. A policy that names a surface its approvers can't bind on is rejected at save time.

Because route and surface are independent, you mix and match them per policy:

Route (reach)Surface (approve)What it means
SlackSlack (yes/no)nudge in Slack, they click right there — fast, low ceremony
SlackDocuSignnudge in Slack, but the binding approval is a signed envelope
emailInboxemailed a link, they decide in the dashboard
phone + emailDocuSignreach on two channels, sign to approve

That last distinction is the whole reason these are separate concepts: DocuSign is not "another channel," it's a surfacehow the human commits the decision, with legal weight, regardless of how they were notified. A policy like "$50k+ payouts must be signed, not clicked" is a statement about the surface, not the route.

Both are configured per policy via the route and surface fields; the policy floor can also require a stronger surface for high-stakes decisions.