workflows, one or more
workflow_triggers, one or more workflow_actions. Every operator
can add or edit them via /api/automations/ (or the admin UI in a
follow-up); no Go change needed.
Automations sit next to two other engines with distinct jobs, and the
three are easy to mix up if you skim:
If you’re just tagging documents by keyword, use rules — they
were built for that and they run on every post-ingest. If you need
to trigger multiple metadata changes together (assign owner + assign
tags + set storage_path when a doc with correspondent “Landlord”
lands), use automations. If you need a person to click Approve,
use approvals.
1. Concepts
Three tables (seecore/db/migrations/0020_automations.sql):
workflows(id, name, order_index, enabled, ...)— one row per automation.enabled=0skips evaluation entirely.workflow_triggers(workflow_id, type, filter_*)— one or more per automation. Every non-null filter must match for the trigger to fire.workflow_actions(workflow_id, order_index, kind, params_json)— one or more per automation. Actions run inorder_indexorder inside a single write transaction.
2. Triggers
Three trigger types. Each has an integer wire code (matches the mobile-app shape) and a semantic name:
Every trigger row supports these optional filters. Non-null fields
are AND-combined; a null field means “match anything”.
3. Actions
Every action carries akind and a small JSON params bag. Actions
are idempotent — re-running the same automation on the same doc
converges rather than duplicating.
Built-in: “Auto-file from archive”
suchi ships one system automation on first boot: Auto-file from archive (system_slugauto_file_from_archive). It’s an
apply_from_similar action on document_added — the archive-based
counterpart to the LLM classifier.
- Toggleable — flip
enabledoff in/automationsif you prefer manual filing. - Editable — tune every threshold and the field set through the visual builder or the JSON view. The action’s own defaults are the source of truth; leaving a param blank falls back to them.
- Undeletable — the SPA hides the trash button;
DELETE /api/automations/{id}returns409 system_automation. Toggling it off is the substitute.
system=1 column on
workflows) — future built-ins ship the same way. The seeder is
idempotent by system_slug, so re-runs and upgrades are no-ops.
4. REST surface
Admin-only writes; any authed user can list/get.4.1 Example — auto-tag Landlord docs and route to a folder
4.2 Example — title template on any invoice from Acme
4.3 Trigger type as string
The wire accepts either the integer code (mobile-compat) or the enum string form. The following are equivalent:5. Evaluation model
document_added— called frompostingest.postContentStepsafter the rules classifier runs and content is loaded. Each matching automation runs its actions in one write tx per automation. A failure in an action logs and moves on.document_updated— called fromPATCH /api/documents/{id}after the doc’s fields land.document_updatedtriggers only fire on successful writes. Automations do NOT re-trigger on their own writes (see §7).consumption— fires at the start ofpostingest.Handle, before any content extraction runs. Producers plumb filter context onto the job payload:POST /api/documents/→filename(multipart Filename)- fs-watch →
filename+source_path(absolute path) - mail-intake →
filename(subject line as proxy;mail_rule_idstays zero until Phase 6 adds mail-rules)
assign_document_type does not re-trigger document_updated on
that same doc — otherwise you’d need a cycle detector. If you need
one automation to depend on another, put both actions in the same
automation.
6. Interaction with rules
Rules run first insidepostingest. Automations see the post-rules
metadata. This ordering is intentional: rules do the light
classification (add_tag, set_correspondent by name-match), then
automations run richer multi-step orchestration that can rely on
those classifications being in place.
7. Audit and observability
Every action logs atINFO under
automations.action.{ok|error} with the doc_id and action kind.
Errors surface at WARN. See privacy — automation runs
are visible in the audit log.
8. Not (yet) covered
- No condition DSL / expression language.
filter_content_matchingis a case-insensitive regex; nothing more sophisticated. If you need arbitrary boolean logic, write two automations. - No user notifications on match — surface via the audit log or wire through a Subscriber plugin.
- No scheduled/time-based triggers. Everything is event-driven.