Skip to content

Universal Action Dialog Specification

Status

Implementation-ready spec for the frontend universal action/event submission dialog. This builds on the completed canonical input/EventBuilder foundation:

  • signed action envelopes use canonical input
  • optional gate resolution uses input.outcome.{value,reason}
  • observations/results live in input.fields.*
  • shared event-builder/ owns envelope construction, canonicalization, signing, and hash preparation

Goal

Provide one reusable frontend dialog for all manual business actions that submit events through process_event.

The dialog must:

  1. render action context and canonical input fields
  2. preview the exact signed action/event details before submission
  3. submit only via process_event
  4. use the shared EventBuilder path for canonicalization/signing/hash
  5. avoid TypeScript workflow semantics beyond rendering server/Rust-WASM supplied action availability and declarative input metadata

Non-goals

  • Do not evaluate workflow roles/guards/transitions in TypeScript.
  • Do not hardcode business action lists in UI.
  • Do not introduce legacy data or action.data.*.
  • Do not replace workflow deployment DeployModal; this dialog is for manual business actions. A later refactor may unify preview components.
  • Do not solve full rich-form/schema design in this milestone; start with deterministic canonical-section fields and a small schema subset.

Current state

Existing pieces:

  • event-builder/ exposes normalizeInput, buildActionEnvelope, prepareActionEvent, prepareActionEventAsync.
  • frontend-react/src/crypto/actions.ts exposes:
  • prepareAction(processId, actionName, workflow, input, prevHash?)
  • submitEvent(actionJson, header, eventHash)
  • signAndSubmit(...)
  • frontend-react/src/pages/Customers.tsx currently submits create directly with signAndSubmit(...) and an ad hoc form.
  • frontend-react/src/pages/WorkflowEditor.tsx uses prepareAction(...) + submitEvent(...) for workflow deployment and DeployModal for deploy preview.

Missing:

  • no ActionDialog/universal manual action dialog component
  • no generic canonical input editor
  • no dialog-level preview of action JSON/signature/hash for manual actions
  • no generic input inference from workflow action definitions

Component contract

Create frontend-react/src/components/ActionDialog.tsx.

```ts import type { ActionInput } from '../../../event-builder/index.mjs';

type ActionDialogProps = { open: boolean; title?: string; workflowPath: string; actionName: string; processId: string; currentState?: string; latestHash?: string | null; actionDefinition?: Record; inputSchema?: ActionInputSchema; initialInput?: ActionInput; onCancel: () => void; onSubmitted: (result: unknown) => void | Promise; };

type ActionInputSchema = Record;

type ActionInputField = { type?: 'string' | 'number' | 'boolean' | 'date' | 'enum' | 'textarea' | 'reference' | 'json'; label?: string; required?: boolean; section?: 'fields' | 'refs' | 'outcome' | 'note' | 'evidence' | 'amounts' | 'dates' | 'flags' | 'meta'; options?: string[]; target?: string; placeholder?: string; help?: string; }; ```

The component owns local input state and computes preview via existing prepareAction(...) from frontend-react/src/crypto/actions.ts.

Dialog flow

  1. Open with workflow/action/process context.
  2. Build an input field model from explicit metadata if provided; otherwise infer from actionDefinition.
  3. Render canonical input sections.
  4. On input changes, normalize into an ActionInput shape.
  5. Generate preview by calling prepareAction(...) with current ActionInput and latestHash.
  6. Show preview fields:
  7. workflow path
  8. action name
  9. process id
  10. latest hash
  11. registry hash
  12. actor public key
  13. canonical action JSON
  14. signature
  15. event hash
  16. Require explicit confirmation checkbox: "I understand this will append an immutable signed event."
  17. Submit by calling submitEvent(preview.actionJson, preview.header, preview.eventHash).
  18. On success call onSubmitted(result).

Preview failures should be visible and fail closed; submission disabled until preview succeeds.

Canonical input rendering

The dialog renders sections in this order:

  1. fields
  2. refs
  3. outcome
  4. note
  5. evidence
  6. amounts
  7. dates
  8. flags
  9. meta

Minimum field support for this milestone:

Schema type UI control Output
string / omitted text input string
textarea textarea string
number number input number if parseable, otherwise string preserved
boolean checkbox boolean
date date input string
enum select selected string
reference text input initially string in refs
json textarea parsed as JSON JSON value

Empty optional fields are omitted. Empty sections are removed by EventBuilder normalization.

Input schema source

Preferred source, if present:

json "input": { "fields.customer_name": { "type": "string", "label": "Customer name", "required": true }, "outcome.value": { "type": "enum", "options": ["approve", "reject", "investigate"], "required": true }, "refs.case_id": { "type": "reference", "target": "case/review/kyc-review" } }

The dialog treats this metadata as UI help only. The engine remains authoritative.

Input inference fallback

If inputSchema is absent, infer fields by scanning the action definition JSON for string expressions containing input.<section>.<key> and input.<section> ? '<key>'.

Inference rules:

Expression pattern Field path Default type
input.fields.foo fields.foo string
input.refs.case_id refs.case_id reference
input.outcome.value outcome.value string unless branch values imply enum
input.outcome.reason outcome.reason textarea
input.note.content note.content textarea
input.dates.date_of_birth dates.date_of_birth date
input.flags.override flags.override boolean
input.fields ? 'foo' fields.foo string

Enum inference for outcome.value:

  • scan branch when expressions for input.outcome.value = 'x' or input.outcome.value = "x"
  • if values exist, render as an enum/select with those options

Do not infer from state references or bare identifiers. Do not evaluate expressions.

Data/API requirements

The first implementation may use existing API shapes:

  • workflow details from getWorkflow(workflowPath) for action definition + optional action input metadata
  • processes from getProcesses(...) or parent page data for chain_tip / prevHash
  • prepareAction(...) for preview
  • submitEvent(...) for submission

If process-specific action availability is not yet exposed by the server/Rust-WASM, the dialog must be invoked only by pages/components that already have a server/WASM-provided available action. Do not compute availability in the dialog.

First integration target

Integrate first with frontend-react/src/pages/Customers.tsx create flow:

  • replace the ad hoc create modal/form with ActionDialog
  • pass workflowPath=activeWfPath
  • actionName="create"
  • processId=crypto.randomUUID() generated when opening the dialog
  • prevHash supplied from the active process/registry chain_tip; EventBuilder normalizes root bootstrap cases to ZERO only where valid
  • derive inputSchema from workflow metadata $fields for create action if no action-level input metadata exists
  • on submit success, close dialog and refresh list

This is intentionally the smallest proving path. Follow-up integrations can add process-detail action panels once server/WASM available-actions output is formalized.

UX requirements

  • Clear immutable-event warning before submit.
  • Preview must show canonical JSON, signature, event hash, and actor public key.
  • Disable submit while preview is invalid or confirmation unchecked.
  • Show validation/preview errors inline.
  • Show submitted result or error; refresh parent on success.
  • Keep styling consistent with existing modal patterns (DeployModal can be referenced for preview layout, but do not force coupling).

Tests

Add tests under frontend-react/src/__tests__/.

Required tests:

  1. renders inferred/schema fields grouped by canonical section
  2. updates preview when input changes
  3. submits process_event using EventBuilder output and canonical input
  4. request body contains no legacy data
  5. outcome.value branch values render as enum options when inferred from branches
  6. required fields disable submit until populated

Minimum gate:

bash cd frontend-react && npm test -- --run && npm run build

Implementation phases

Phase 1 — Component + unit tests

Files likely touched:

  • frontend-react/src/components/ActionDialog.tsx
  • frontend-react/src/__tests__/action-dialog.test.tsx
  • optional small helper file: frontend-react/src/components/actionInputModel.ts

Phase 2 — Customers create integration

Files likely touched:

  • frontend-react/src/pages/Customers.tsx
  • tests for customer create flow if existing harness supports it

Phase 3 — Process detail integration (follow-up)

Deferred until server/Rust-WASM action availability output is formalized. The dialog should be ready to consume available action descriptors but must not invent availability.

Descriptor contract: docs/architecture/available-action-descriptors-spec.md.

Guardrails

  • Every submitted mutation goes through process_event.
  • Use EventBuilder/prepareAction for preview; do not duplicate canonicalization/sign/hash logic in the component.
  • No legacy data in action envelope or request body.
  • No TypeScript workflow guard/role/transition evaluation.
  • SQL/API calls remain parameterized/server-side.
  • Actor private key handling remains whatever frontend-react/src/crypto/actions.ts currently centralizes; do not add keys to component state beyond calling helpers.