Workflow orchestration of e-signature + e-Meterai, and user custom workflows — design
Status: approved direction (brainstormed 2026-07-14); spec for review before planning.
Goal
The workflow engine orchestrates complete document ceremonies end-to-end:
- Signature steps — internal signers and external (client) signers — in any order.
- e-Meterai affixing as a routed duty, so a requester WITHOUT
meterai.affix
can still run a flow that ends in a stamp (a permitted person performs it). - Requesters can design custom workflows (ad-hoc, run-once) without admin help;
a later phase lets them save these (private/public) under admin kill-switches.
Reference use cases (all must be expressible):
- (a) secretary: sign(PM) → sign(client, external) → meterai
- (b) same as (a) but the secretary lacks
meterai.affix - (c) PM: sign(self) → sign(higher-up) → sign(client, external)
- (d) (c) + meterai
- (e) (d) but the PM lacks
meterai.affix
Locked decisions
- No auto-affix, no auto-sign — ever. Every signature and every stamp is a human
task completed through the existing ceremonies (OTP/placement/two-click). The
engine routes and waits; it never acts on a document by itself. - Position is mandatory and requester-chosen for internal sign steps and meterai
steps. The requester names WHO should act (a position, expanded to its live
holders at fan-out). No blank assignment, no engine-chosen assignee. - Overrideable by the acting user. Someone other than a named position's holder
may complete the step, recorded as anoverridetransition (chain-of-custody
names who actually acted): - Sign step: override requires out-ranking the named position via the
existingOverrideAuthority(a signature is identity-bearing; only a superior
may sign in the named position's stead) — same rule approve steps use today. - Meterai step: override requires holding
meterai.affix(the stamp is an
org duty, not an identity; any permitted user may perform it). No rank check. - The intrinsic gates always apply on top: document access for signing,
meterai.affix+ quota for stamping. Override never bypasses RBAC.
Step model (extends the existing sequential engine)
The engine keeps its shape: ordered StepSpec[], versioned definitions, instances
pin a version, one step active at a time, tasks fan out to position holders.
StepSpec grows (jsonb-backward-compatible, like kind was):
{
"name": "Client signs",
"kind": "approve" | "sign" | "sign_external" | "meterai", // 2 new kinds
"approver_position_ids": ["..."], // approve/sign/meterai: mandatory, ≥1
"contact_slot": "Client signatory", // sign_external only: label shown in the start dialog
"sign_kind": "internal"|"global"|"psre" // sign/sign_external: suggested tier (hint, like today)
}
sign(existing) — unchanged semantics; internal signer(s) from the named
position; completed by the existingCompleteSignatureForSubjecthook.
"Sign by me first" (case c) = the existing per-step recommended-user mechanism;
the initiator recommends themself on step 1.sign_external(new) — the definition declares a labeled contact slot; the
start dialog collects name/email/phone for each slot (a template cannot know
the client's address). On activation the engine mints a ONE-signer envelope via
the existing envelope/invite machinery (public tokened link, OTP, attestation or
Global tier); envelope completion advances THIS step (per-step advance, not the
whole-instanceFinalizeInstancethe standalone request uses). Start-time
contacts are stored on the instance (likeRecommended), not the definition.meterai(new) — holders of the named position get an "affix e-Meterai"
inbox task (action_required: "meterai"). They open the document and run the
EXISTING affix ceremony (placement picker, two-click confirm, claim→provider→
ledger). A new hookCompleteMeteraiForSubject(p, subjectType, subjectID)—
mirror of the sign hook, called by the affix handler after success — completes
the step and advances. Recorded as a newActionMeteraihistory entry.
Validation rules (definition save + start)
meteraistep must come after at least one sign step (provider refuses to
stamp an unsigned document; the affix service already enforces signed-first).
At most one meterai step per definition (one-stamp-per-document ledger rule).sign/meteraisteps: ≥1 position id (existing rule).sign_external: non-empty
contact_slot; positions list must be empty.- Fan-out of a
meteraistep fails loudly (workflow.step.no_affixers) if no
resolved holder actually holdsmeterai.affix— a misconfigured position is an
authoring error, not a silent dead workflow. - Starting a definition containing
sign_externalsteps requires one contact per
slot; email format validated; phone optional (channel selection mirrors the
standalone external request). - Module gating: sign/sign_external/meterai steps require the
esignmodule
(existingrequireSignLicensedpattern extends to the new kinds).
Custom workflows (requester-designed)
Phase: run-once (ad-hoc). Any user holding a new workflow.custom
permission can open a lightweight builder (same step palette: approve / sign /
sign_external / meterai; same validation), assign positions per step, pre-set the
sign/meterai positions (overridable at act time per the rules above), then RUN it
immediately on a document they can write. Mechanically this mints a definition
with kind: "custom" + one version + an instance — exactly the shape the
standalone signature request already uses (kind: "signature_request"), so the
engine, inbox, graph view, history and cancellation all work unchanged. Ad-hoc
definitions are hidden from the admin designer list (like signature requests).
Phase: saving + sharing. A custom definition can be SAVED for reuse:
workflow_definitionsgainsowner_id(nullable; set for custom kind) and
visibility text NOT NULL DEFAULT 'private'(private|public).- Private: only the owner lists/starts/revises it. Public: every
workflow.startholder can list/start; only the owner (or an admin) revises. - Saved customs appear in a "My workflows / Shared workflows" picker next to the
admin templates; instances pin versions as always, so later edits never touch
in-flight runs.
Admin kill-switches (new workflow_settings singleton row + Admin tab,
mirroring the auth-settings pattern):
custom_workflows_enabled(default on) — off: the builder and ad-hoc run
endpoints refuse (workflow.custom.disabled); in-flight instances keep running
(they pin versions; disabling prevents NEW runs, never strands work).custom_workflow_saving_enabled(default on) — off: run-once still works,
save/publish refuses; already-saved customs stay startable (admins can archive
them individually with the existing archive mechanism if needed).
What does NOT change
- The approval state machine (submitted → approved/rejected/returned), SLA/
escalation, dispose/forward, sub-workflows, substitution — all untouched; the
new kinds ride the sameadvanceApprovedInstancepath. - The standalone "Request signature" flow keeps working as-is (it becomes the
simple case of the same machinery). - e-Meterai remains claim→provider→finalize with the same quota/ledger; the
workflow only creates the task and listens for completion.
Build order
meteraistep kind — domain kind + validation, fan-out task, affix-handler
hook, override rule (perm-based), designer + start UI, inbox card linking to the
affix ceremony. Covers (b)(d)(e) with the existing external request for clients.sign_externalstep kind — start-time contact slots, envelope-per-step
mint + per-step advance on envelope completion, invite resend/cancel plumbing.
Completes (a)(c) inside one definition.- Ad-hoc custom workflows —
workflow.customperm, builder UI (reuses the
designer's step editor),kind: "custom"mint + immediate start. - Saving/sharing + admin switches — owner/visibility columns (migration
00095+), workflow_settings + Admin tab, My/Shared pickers.
Each phase ships independently behind the existing module/permission gates.
Open questions (to resolve during implementation, none block the plan)
- Does the affix-precondition ("officially signed") accept an internal-tier
signature, or only official tiers? Case (a) signs internally + client attestation
before meterai — verify the service predicate matches, relax if it demands an
official tier the flow doesn't produce. - Envelope-per-step reuses the existing envelope tables; confirm a second
sequentialsign_externalstep (two clients, ordered) can mint two envelopes on
the same subject without colliding with the roster-finalize path. - Whether
workflow.customshould be seeded into existing roles by the migration
(probably: yes for the roles that already holdworkflow.start).