think
16px
820px

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:

  1. Signature steps — internal signers and external (client) signers — in any order.
  2. 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).
  3. 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 an override transition (chain-of-custody
    names who actually acted):
  • Sign step: override requires out-ranking the named position via the
    existing OverrideAuthority (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 existing CompleteSignatureForSubject hook.
    "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-instance FinalizeInstance the standalone request uses). Start-time
    contacts are stored on the instance (like Recommended), 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 hook CompleteMeteraiForSubject(p, subjectType, subjectID)
    mirror of the sign hook, called by the affix handler after success — completes
    the step and advances. Recorded as a new ActionMeterai history entry.

Validation rules (definition save + start)

  • meterai step 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/meterai steps: ≥1 position id (existing rule). sign_external: non-empty
    contact_slot; positions list must be empty.
  • Fan-out of a meterai step fails loudly (workflow.step.no_affixers) if no
    resolved holder actually holds meterai.affix — a misconfigured position is an
    authoring error, not a silent dead workflow.
  • Starting a definition containing sign_external steps 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 esign module
    (existing requireSignLicensed pattern 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_definitions gains owner_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.start holder 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 same advanceApprovedInstance path.
  • 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

  1. meterai step 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.
  2. sign_external step 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.
  3. Ad-hoc custom workflowsworkflow.custom perm, builder UI (reuses the
    designer's step editor), kind: "custom" mint + immediate start.
  4. 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
    sequential sign_external step (two clients, ordered) can mint two envelopes on
    the same subject without colliding with the roster-finalize path.
  • Whether workflow.custom should be seeded into existing roles by the migration
    (probably: yes for the roles that already hold workflow.start).