AID Docs

Why an Automation refuses to turn on

The nine conditions that refuse an Automation activation, what each one means in plain language, and why a saved Automation can still be running an older version.

Decision and implementation status: ADR-0025 is Accepted and defines the Save / On-Off / delete surface described here. The nine refusal conditions below are implemented in the aid_activate_automation_definition stored procedure and surfaced by the HTTP routes. One part of ADR-0025 is not currently implemented: the persistent operational status text. See The status the screen no longer shows.

Start here: storing and activating are different

When an Automation is on, or the author is turning it on, Save may do two separate jobs in one transaction:

  1. Store the configuration as a new immutable version.
  2. Activate that version, so it becomes the one that runs from now on.

Two "may"s worth knowing before you read on. Saving an Automation that is off stores without activating — Save never turns an Automation on by itself. And storing only appends a new version when the effective configuration actually changed; re-saving identical content reports success without creating one.

Almost every confusing Automation support question comes from not knowing that storing and activating are separate. Job 1 can succeed while job 2 is refused, and when that happens you get a state that looks wrong but is working exactly as designed:

The Automation is switched on. Your new configuration is saved. And the version actually doing work is the old one.

The server can also refuse the whole request before job 1 happens at all, in which case nothing was saved and the author still has their work only on screen. Telling those two apart is the point of Three outcomes when Save also attempts activation, and getting it wrong is how someone ends up assuring a colleague their work is stored when it is not.

This page explains every way the request can be refused, and why AID behaves this way.

Why they are separate at all

Storing and running are different kinds of promise. Storing says "this is what you wrote down." Running says "this is safe to unleash on real documents right now." A configuration can be perfectly valid to keep and still be unsafe to start — because it collides with another Automation, or because permission for one of its steps was withdrawn this morning.

Publication has never meant activation in AID (ADR-0018). This page is that principle showing up where users can feel it.

The mental model that makes it click

A version only runs if it has an activation interval — a record saying "from this moment onward, this version is the live one."

The dispatcher picks which version handles an event by looking at those intervals:

// apps/api/src/domain/automation/dispatch-planner.ts
const effectiveForEvent = (definition, eventCommitOrder) =>
  definition.effectiveOrder <= eventCommitOrder &&
  (definition.disabledOrder === null || definition.disabledOrder > eventCommitOrder);

effectiveOrder comes from the activation record. So:

A version with no activation record never runs. Not for old work, not for new work, not ever — until something activates it.

This is the single most misunderstood point. Junior engineers often assume a refused activation means "the new version applies to new documents, old ones finish on the old version." That is what happens on a successful activation. A refused one is different:

OutcomeNew documents runAlready-started runs
Save succeeds, activation succeedsv2v1 (pinned at start)
Save succeeds, activation refusedv1v1

In the second row, v2 sits in the database, correct and complete, doing nothing at all.

Why already-started runs keep the old version

When a run starts, the version it will use is stamped onto it and frozen. That is deliberate: a document halfway through a three-step Automation should not have step 3 behave by different rules than step 1. This part is normal and needs no warning — it is the refusal case that surprises people.

Three outcomes when Save also attempts activation

An Automation that is off has the simpler path described above: Save stores any changed configuration and leaves the Automation off. When Save also tries to activate a version, the server may reject the request before it stores anything. That activation-attempting path has three outcomes:

Save outcomeWas the new version stored?What runs afterward?
Rejected before storageNoUnchanged — whatever was running, or nothing
Stored, activation refusedYesPrevious version
Stored and activatedYesNew version
  1. Rejected before storage. Nothing new is saved and runtime state is untouched — which means the previous version if one was running, and nothing at all for an Automation that is off, brand new, or already deleted. The Save command checks the author's on/off token, the Automation's references, and its capability authority before writing anything, so a stale editor, a retired dependency, or a withdrawn permission stops the request early.
  2. Stored but not activated. The new version is saved and the previous version keeps running. This is where conflict analysis and conflict review refuse the activation half.
  3. Stored and activated. The new version becomes the one used for future events.

The important support question is therefore not only "why was activation refused?" but also "did the response include a newly saved version?" Rows 1 and 2 look similar on screen and are completely different in what survives.

Row 2 is why the API response keeps the saved version's identity in a separate, nested place from the activation command's own fields:

// apps/api/src/domain/automation/definition-routes.ts — the refusal branch
// Nested exactly as the accepted branch nests it. Spread flat, the
// activation command's own `automationDefinitionVersionId` and
// `revision` — null on every refusal — would overwrite the version
// this Save durably committed and the head revision it advanced,
// and the author would be told their saved work does not exist.

Read that comment's "null on every refusal" as shorthand rather than a guarantee. The activation procedure returns null activation fields for most refusals, but for revision_conflict, needs_repair, and capability_disabled it deliberately returns the currently active version and revision instead. So flattening the response would corrupt the answer in one of two ways — either blanking the version the Save just committed, or overwriting it with the older version that is still running. The nesting is what prevents both.

The nine refusal conditions

These come from aid_activate_automation_definition. The right-hand column is the one to read first: it says what the outcome means for the author's work in the normal Save flow, using the three outcomes above.

#Database resultCodeAPI reasonOne-line meaningNormal Save effect
1not_foundnotFoundThe Automation is gone.Rejected before storage
2version_not_foundversionNotFoundThat saved version is gone.Defensive integrity result only
3revision_conflictactivationConflict / revisionConflictSomeone else changed on/off while you worked.Normally caught before storage
4needs_repairneedsRepairSomething it depends on was retired or trashed.Normally caught before storage
5capability_disabledcapabilityDisabledPermission for one of its steps was withdrawn.Normally caught before storage
6conflict_blockedincompatibleConflictProvably fights with another live Automation.Version remains saved
7conflict_limit_exceededconflictReviewLimitExceededToo many possible overlaps to review safely.Version remains saved
8conflict_review_requiredconflictReviewRequiredPossible overlaps nobody has acknowledged yet.Version remains saved
9conflict_review_staleconflictReviewStaleThe overlap evidence you acknowledged has changed.Version remains saved

Why the difference: the Save command runs its own checks on the on/off token, references, and capabilities before it writes the version, so conditions 3 to 5 normally stop the request early. The stored procedure still checks them again as a defensive re-check — for the standalone activate route, and to close a race where something changed between the Save command's check and the activation. So seeing one of those codes from the procedure is possible but unusual, and only the conflict family (6 to 9) is a routine "your version is saved, but the old one is still running" outcome.

The two success codes, for completeness, are activated (a new activation opened) and unchanged (that version was already the live one — pressing Save twice is not an error).

Throughout the examples below, the cast is: Aisha, an office manager with no IT background; Farid, a workspace administrator; Mei, in finance.


1. not_found — the Automation is gone

What the database checks: no Automation head row with that ID exists in this workspace.

Real scenario. Aisha has the Invoice naming dialog open on her laptop. Farid, tidying up on his own machine, deletes that Automation because a replacement exists. Aisha, unaware, presses Save.

There is no longer an Automation to attach a version to, so the request is refused. Nothing Aisha did was wrong; her screen was simply describing a world that stopped existing a minute ago.

What to do: copy her edits out of the dialog first, then reload — pressing Save again cannot help, because there is nothing to save into. Deleting also closed the activation, so the Automation is not running either. If the deletion was a mistake, the configuration has to be recreated: deletion in AID is a tombstone (ADR-0025), so history is retained for audit, but the Automation does not return to the list on its own.


2. version_not_found — that saved version is gone

What the database checks: the requested version ID is not a published version of this Automation.

Normal authoring should never produce this. The Save command does not send a version ID from the browser — it publishes the stored draft and hands the ID it just created straight to activation, inside the same transaction. There is no window in which the browser's idea of a version ID matters.

The workspace activation route does accept a version ID, but it first checks that the ID is the newest saved version. A wrong, foreign, missing, or superseded ID is therefore refused as versionSuperseded before this procedure runs. The authoring dialog uses that route while resolving conflict reviews; it is not an operator rollback tool, and no such operator surface exists yet.

Expected scenario: none through the supported HTTP flows. This is a defensive stored-procedure result for an integrity invariant the routes are designed to establish before calling it.

What to do: investigate rather than retry. Seeing version_not_found means the route and procedure disagreed about a version inside one transaction, or that an unsupported caller invoked the procedure directly.


3. revision_conflict — someone else changed on/off meanwhile

What the database checks: the activation ID and revision the request expects do not match what is stored. This is optimistic concurrency: the request says "I believe this Automation is currently off" and the database disagrees.

Note the mapping quirk: the standalone activate route calls this revisionConflict, while the Save route calls it activationConflict. Same database condition, two names, because in the Save flow it is specifically the on/off state that moved under the author's feet.

Real scenario. Mei reports that invoices are being renamed wrongly, so Farid turns Invoice naming off from his desk at 14:02. At 14:02 Aisha — who opened the dialog at 13:55 and has been typing since — presses Save with the switch still showing on.

Without this check, Aisha's save would silently re-activate the Automation Farid just switched off to stop a live problem. The refusal exists so one person's stale screen cannot undo another person's deliberate emergency action.

What happens to Aisha's typing: nothing is stored. In the Save flow this is checked before anything is written, so her edits stay only in the browser — she must not close the dialog assuming they were saved.

What to do: copy Aisha's edits somewhere safe before reloading. There is no merge — reloading starts from server truth, which is why the dialog guards it with a confirmation reading "Your unsaved changes will be lost" and a button labelled Discard and reload. Once the edits are safe, load the current on/off state and decide with the fresh picture. This is the one refusal that is almost always protecting you from a real mistake.


4. needs_repair — something it depends on was retired

What the database checks: every dependency the version declared still resolves. Specifically it looks for:

  • a folder that has been trashed (trashed_at is set);
  • a document type that is no longer active;
  • a field definition that is no longer active.

Real scenario. The Invoice naming Automation renames files using a template like Invoice-{Supplier}-{Date}.pdf, where Supplier is a custom field. During a metadata cleanup, Farid retires the Supplier field because he thinks nothing uses it.

Aisha later opens the Automation, fixes an unrelated typo, and presses Save. The request is refused, because the Automation now refers to a field that no longer exists — letting it run would produce garbage filenames on real invoices.

Why refuse instead of running it degraded? Because the failure would be silent and spread across every future document. A refusal is loud, immediate, and affects nothing.

What happens to Aisha's typo fix: nothing is stored. The Save command inspects references before writing, so this lands in "rejected before storage."

What to do: either restore the retired dependency, or edit the Automation so it no longer references it, then Save again.


5. capability_disabled — permission for a step was withdrawn

What the database checks: every capability dependency of the version has a matching grant in workspace_automation_capability_grant with disabled_at null.

AID grants authority per capability, not globally (ADR-0018). An OCR-only Automation does not need rename authority, and a rename Automation does not gain OCR authority.

Real scenario. OCR costs money per page. During a budget review, Farid turns off the workspace's OCR capability while the finance team decides whether to keep paying for it. Two weeks later Aisha opens an Automation that includes an OCR step, adjusts its document type, and saves.

The request is refused, because turning it on would start spending money on a capability the workspace has explicitly withdrawn.

Notice what this scenario does not involve: any second Automation, any conflict, anything Aisha did wrong. This is the clearest proof that refusals are not only about Automations fighting each other.

What happens to Aisha's edit: nothing is stored. Capability authority is checked before the version is written.

What to do: re-enable the capability (the dialog offers this directly when the capability is registered), or remove the step that needs it.


6. conflict_blocked — it provably fights another live Automation

What the database checks: a deliberately narrow, provable hard conflict. All of these must hold against an Automation that is currently on:

  • the same exact scope;
  • the same condition set;
  • exactly one rename step on each side;
  • both rename templates fully static (no placeholders — every segment is a literal);
  • and the two resulting names are different.

That is a case where the two Automations cannot both be right about the same document, and no data-dependent guesswork is needed to prove it.

Real scenario. The Reports folder has an Automation renaming everything to Monthly-Report.pdf. Aisha, not knowing it exists, builds a second one on the same folder with the same conditions renaming everything to Report-Final.pdf.

Two Automations, same documents, two different fixed names. Whichever runs second overwrites the first, and the outcome depends on ordering nobody can see. AID refuses rather than letting them fight.

What to do: turn the other Automation off, or narrow one of them so they no longer describe the same documents.


7. conflict_limit_exceeded — too many possible overlaps to review

What the database checks: more than 50 other live Automations classify as possible_overlap with this one.

Real scenario. A large workspace has one Automation per document type — around sixty of them. Aisha creates a broad folder-scoped Automation over a folder that receives all of those types. Every one of the sixty could potentially touch the same document.

AID could ask her to review sixty overlaps one at a time. It refuses instead, because a sixty-item review is not a review — nobody reads item forty-one carefully, and a rubber-stamped acknowledgement is worse than no acknowledgement, since it creates a false record that someone checked.

What to do: narrow the scope or conditions so fewer Automations can collide. The limit is a signal that the Automation is too broad to reason about, not an arbitrary ceiling.


8. conflict_review_required — overlaps nobody has acknowledged

What the database checks: at least one possible_overlap exists that has no matching acknowledgement in the request.

This is the largest and most interesting category, because "possible overlap" is the honest answer whenever AID cannot prove the two Automations are safely separate.

Here is the part that surprises people. Scopes are only treated as disjoint when they are the same kind:

// apps/api/src/domain/automation/conflict-analysis.ts
const scopesAreDisjoint = (left, right) =>
  left.kind === right.kind &&
  (left.kind === "folder"
    ? left.folderId !== right.folderId
    : left.documentTypeId !== right.documentTypeId);

A folder-scoped Automation and a document-type-scoped Automation are therefore never disjoint — because a document lives in a folder and has a type, so one document can genuinely be hit by both.

Real scenario. The Invoices folder has an Automation that renames incoming invoices. Separately, a workspace-wide Automation runs OCR on every document of type Scanned Contract. A scanned contract filed into the Invoices folder matches both.

AID cannot prove from configuration alone whether they will actually collide on a given document — that depends on real data. So it asks a human to look at a test document and confirm, rather than guessing.

A common misconception worth stating plainly: "we only allow one Automation per folder, so conflicts cannot happen." That is not true, for two reasons. First, nothing in the system enforces one Automation per folder — the unique index is one active version per Automation, not one Automation per folder, and multiple Automations running on one document is a designed feature with an explicit ordering. Second, even a strict one-per-folder policy does not stop a document-type Automation from reaching into that folder.

What to do: run the conflict review the dialog offers, look at the test document, and acknowledge it. The acknowledgement is recorded evidence that a person looked.


9. conflict_review_stale — the evidence you acknowledged has changed

What the database checks: that the evidence attached to the request still describes the situation it was gathered from. The evidence is stale when the other Automation changed or stopped overlapping, or when the test document changed, disappeared, or no longer demonstrates the same overlap.

Real scenario. Aisha starts a conflict review against the Scanned Contract Automation and acknowledges the overlap. While she is still working, Farid turns that Automation off. Aisha presses Save.

Her acknowledgement now describes a conflict that no longer exists. AID refuses rather than accepting evidence about a world that has moved on — an acknowledgement is an audit record saying "a person examined this specific situation," and it must not be reusable against a different situation.

What to do: run the review again. The refusal names the exact stale acknowledgement so an administrator can remove it and converge.


What the user sees, and how they recover

All nine show a specific error message naming the reason. What to do next is not the same for all of them, and pressing Save again is the right move for only two of the four groups.

Condition 1 — the Automation was deleted. Save cannot recover this; there is nothing left to save into, and deleting also closed the activation, so nothing is running. Copy any local edits out of the dialog first, then reload. If the deletion was a mistake, the work has to be recreated — deletion is a tombstone, so the history remains for audit but the Automation does not come back on its own.

Condition 2 — an activation integrity invariant failed. Supported Save and standalone activation requests establish that the version exists before this procedure runs. Investigate why the route and procedure disagreed, or whether an unsupported caller invoked the procedure directly; do not treat this as a normal retry.

Conditions 3 to 5 — rejected before storage. The Automation is untouched and its previous version, if any, keeps running. The author's edits exist only in the browser, so the most useful thing to tell them is "don't close this dialog yet." Copy those edits out before any reload — reloading discards them, which is what the Discard and reload confirmation is warning about. Then reconcile the cause — load the current on/off state, restore or drop the retired dependency, or re-enable the capability — and Save.

Conditions 6 to 9 — stored but not activated. The version is safe on the server and the previous version keeps running. Resolve the conflict or complete the review, then Save. Here Save is specifically a retry: it stores nothing it has already stored and asks the server to start the newest saved version. That matters because the obvious alternative — turning the Automation off and on again — would close a valid, working activation to fix a problem that was never about running.

That is why Save stays enabled even when the editor has no unsaved edits, as long as the live version is not the newest saved one:

// apps/web/src/components/automation/automation-authoring-dialog.tsx
const saveDisabled =
  bodyState !== "editor" ||
  !valid ||
  busy ||
  (selectedId !== undefined && !isDirty && !runningEarlierVersion);

If you are ever tempted to simplify that last clause, this is what it is for.

The status the screen no longer shows

ADR-0025's Decision section requires persistent operational text — On, Off, Not saved · Off, On · unsaved changes, On · running your previous saved version, Paused by AID support — "never inferred from which buttons are disabled and never carried by colour alone."

The authoring dialog currently does not render that text. It was removed when the operational status box was replaced by a footer-mounted On/Off switch. The consequence is specific and worth understanding as a worked example of why that ADR clause existed:

  1. A conflict-family refusal (6 to 9) happens, so the version is stored but not activated. The user sees the error and closes the dialog.
  2. The error message lives in component state, so it dies with the dialog.
  3. The condition lives on the server, so it does not.
  4. Reopening shows the switch on, the saved configuration in the editor, and no warning anywhere. The screen looks finished. The old version is still running.

The only surviving trace is that Save stays enabled — which is ambiguous, because an enabled Save button normally just means "you have unsaved edits."

Restoring this text is deferred work, tracked in docs/backlog.md under "Restore the Automation operational status surface." ADR-0025 remains the authority on the intended behaviour; the gap is in the implementation, not in the decision.

Where this lives in the code

ConcernLocation
The nine refusal conditionsaid_activate_automation_definition stored procedure, apps/api/drizzle/migrations/
Result code → API reason mappingapps/api/src/domain/automation/definition-routes.ts (activate route and Save route)
Which version runs for an eventapps/api/src/domain/automation/dispatch-planner.ts
Overlap classificationapps/api/src/domain/automation/conflict-analysis.ts
Browser messagesautomation.dialog.rejected.* in apps/web/src/i18n/locales/en.json

Decision sources

The canonical records remain the authority when this summary and an implementation detail disagree:

  • docs/adr/0025-automation-save-on-off-and-deletion.md — Save as one transactional command, refusal behaviour, and the operational status requirement.
  • docs/adr/0018-automation-definitions-capability-registry-and-publication.md — publication is not activation, and the per-capability authority model behind refusal 5.
  • docs/adr/0019-automation-dispatch-order-conflicts-and-causation.md — behind refusals 6 to 9.
  • docs/adr/0015-folder-and-document-type-automation-scopes.md — why folder and document-type scopes can reach the same Document.

The stored procedures under apps/api/drizzle/migrations define the currently implemented refusal conditions. Related reading: Automation Activity and audit policy.

On this page