Approval workflows that actually hold
Most tools give you a status field called Approved and call it an approval workflow. The difference shows up the first time somebody edits an approved record — here is what a real one has to do.
Almost every operational tool can show you an approval workflow. Add a single-select field with the options Draft, Submitted and Approved, wire an automation that emails somebody when it changes, and you have the demo.
The demo is not the feature. The feature is what happens on the ordinary Tuesday afterwards — when the person who submitted it edits the amount, when a link scanner opens the approval email, when finance asks who signed off on an invoice from March, when somebody drags the status column back to Draft because they were tidying up.
This is about that Tuesday.
The test
Here is the shortest way to tell a real approval flow from a status field wearing its clothes.
Approve a record. Then change it.
If the record is still approved, you do not have an approval workflow. You have a label — and the moment anybody notices that, the label stops meaning anything to anyone.
Everything below follows from taking that single question seriously.
What a status field cannot do
Anyone can write it
A select field is a select field. Whoever can edit the record can set it to Approved, and so can a CSV import, an automation, a paste of forty rows, and anything holding an API token. In most tools the only thing standing between a draft and an approved record is that nobody happened to click the wrong option.
You can paper over this with permissions, but not cleanly: the same people who need to edit the record's other fields are usually the people you are trying to stop editing this one.
What it takes to fix: the approval state has to be writable by the approval engine and refused everywhere else — at the write endpoints, so that imports, automations, the API and a spreadsheet paste all bounce off it the same way. Not a convention. A refusal.
Approval survives editing
This is the one that quietly destroys trust in the whole system. An invoice is approved at £4,000. Someone corrects a line item. The invoice is now £40,000 and still says Approved, because nothing connected the two facts.
Nobody did anything wrong, and the record is now lying.
What it takes to fix: approving has to seal something, and the seal has to be enforced on every write path — the app, imports, automations, the public API — rather than being a flag that the UI happens to respect. And unsealing has to be a deliberate, recorded act rather than a silent side effect of an edit.
One approver, or a list you maintain by hand
Real approvals have shape. Two people from finance but only one from legal. Anyone on the ops rota. The person named in the record's own Account manager field. Everybody, in order, but only if the amount is over five thousand.
A status field has none of that, so it lives in someone's head or in a wiki page that is out of date.
What it takes to fix: stages with their own approver rules and their own quorum — everyone, any one, or N of M — and routing conditions so a stage only opens when it is relevant.
Nothing knows about the children
An invoice is not really ready to post because the invoice is filled in. It is ready when every line item on it is coded, every one has a nominal account, and none of them is still in draft.
That is a question about related records, and a status field cannot ask it.
What it takes to fix: gates that travel a relation — "every linked line item has a nominal code" — evaluated at the moment of the decision, not the moment somebody remembered to check.
The approval email is a security hole
Corporate mail security follows every link in every message to see where it goes. If your approve link is a GET that approves, the scanner approves it. Before the human has read the subject.
This is not hypothetical; it is the best-known failure mode in this entire product category.
What it takes to fix: the emailed link must land on a confirmation page whose POST does the deciding, so a scanner following the URL changes nothing. The link should carry a signed capability naming which request, which approver, and an expiry — so possession of the link substitutes for logging in, and a forwarded email from two years ago decides nothing.
What "sealed" has to mean
Sealing is the part most implementations skip, because it is the part with teeth.
A posted record should be read-only everywhere — not greyed out in the interface while the API cheerfully accepts a PATCH. That means the seal is checked in the same place every other write rule is checked, and every route in and out of the product inherits it for free: the grid, a paste, a CSV import, an automation step, the public API, an AI agent acting on your behalf.
Three things fall out of doing it properly:
Sealing can cascade, but only where you say. Posting an invoice can seal its line items too, because a sealed total whose components are still editable is not sealed. It should be an explicit choice of which relation to follow, though — locking things the person configuring it never chose is how a well-meaning control becomes the thing everybody works around.
A period is just a lot of records. "Lock everything dated on or before the 31st" should post those records through the same mechanism, not a second parallel kind of lock. One lock is auditable; two locks eventually disagree.
Unposting is a real action with a reason attached. Not an undo. Somebody unsealed a signed-off record, and that fact belongs in the history with their name on it.
Where approval state should live
The temptation is to put the flow in its own object — a workflow run, with its own state, sitting alongside the record.
It is worth resisting. If the approval state is an ordinary field value on the record, then "everything sitting with finance" is a saved view, a board column, a filter, a report axis and an automation trigger — all for free, because it is just data. Put it in a parallel run object and every one of those has to be rebuilt against a second model, and the two drift.
The rule that makes this safe is the one from earlier: the field is ordinary to read and refused to write except through the engine.
What this looks like in RowFold
Everything above is implemented, which is why the article is shaped that way — the requirements came first and the feature answers them.
- Ordered stages, each with its own approvers — named people, a workspace role, or whoever the record's own Person field points at — and its own quorum: everyone, any one, or N of M.
- Routing conditions per stage, so legal only sees what legal should see.
- Relationship gates that travel a link, using the same route vocabulary as filtered rollups rather than a new predicate language. A gate on "every linked line item" blocks a childless invoice too, rather than passing it vacuously.
- Due dates, reminders and escalation — notify, add an approver, reassign to somebody else, or decide it automatically. That last one exists because some teams genuinely need it, and the builder warns you before it will let you turn it on.
- A protected status field. Direct writes are refused at the write endpoints, so a paste, an import, an automation and the API all bounce.
- The seal, enforced on every write path, with optional one-hop cascade to the records the parent contains.
- Period lock — post everything on or before a date, through the same sealing mechanism.
- Decide from the email, on a signed confirmation page whose POST does the work, so a link scanner cannot answer for you.
- A rejection sets the journey back, and you choose whether resubmitting restarts from stage one or resumes at the stage that objected.
None of it is a separate tier, and the whole feature is invisible on tables that do not have a policy — a table with no approvals looks exactly like a table.
When you do not need any of this
Most tables do not. A content calendar does not need a quorum; a task board does not need a seal. Approvals earn their complexity on records that represent a commitment — money leaving, a price going out, a document somebody will be held to.
The honest signal that you need it is not a feature comparison. It is this: you already have an approval process, it is enforced by people remembering, and somebody has been burned. If nobody has been burned yet, a status field is fine and you should keep your life simple.
The checklist
If you are evaluating any tool on this — including this one — these are the questions that separate the demo from the feature:
- Approve a record, then edit it. Is it still approved?
- Can an import, an automation or the API write the approval field directly?
- Can a stage require two of five people, and a different stage require one?
- Can approval depend on the state of related records?
- What happens when a mail scanner opens the approval link?
- Is a locked record locked in the API, or only in the interface?
- Can you lock a whole period in one action?
- Does unlocking record who did it and why?
Seven and eight are where most tools stop pretending. One and five are where most of them fail immediately.