The Invariant
There is one write path and this is it. POST /api/v1/actions/:trigger on the edge node is the sole way a business fact enters the system — from the operator dashboard, a machine integration, or any future client. If you are writing anything that bypasses this endpoint, you are building the bug this architecture exists to prevent.
The Contract
POST /api/v1/actions/:trigger
Authorization: Bearer <operator session>
X-Idempotency-Key: <uuid per logical gesture> # same key = same gesture, safe retry
Content-Type: application/json
{ "aggregateId": "<optional uuid>",
"payload": { /* trigger-declared fields only — context comes from session */ } }
type ActionResult =
| { status: 200, body: object } // applied (row, to_state, …)
| { status: 400, code: 'ACTION_FAILED' } // constraint/validation — fail-closed
| { status: 401, code: 'AUTH_REQUIRED' } // missing/expired session
| { status: 403, code: 'ACTION_FAILED' } // RBAC denial
| { status: 409, code: 'ACTION_FAILED' } // FSM transition conflict
| { status: 409, code: 'IDEMPOTENCY_IN_PROGRESS' } // same key still processing — retry
| { status: 429, code: 'RATE_LIMITED' } // honor Retry-After
| { status: 503, code: 'NO_ENGINE' }; // no manifest loaded on the node
Payload rules: payload is the trigger’s input — tenant_id, operator_id, device_code come from the session ($context), never the body; expressions read it as $payload.*. There is no version optimistic-lock field: ordering is enforced by the FSM — an out-of-state trigger is rejected with 409.
The Flow
client → trigger + payload + idempotency-key
→ edge: session → RBAC → constraint → FSM → data-link → txn(write+audit+outbox)
→ 200: fact committed, outbox entry already durable
retry on network drop: same X-Idempotency-Key → same outcome, no duplicate fact
Payload Example
# QC_PASS — closes the inspection; the constraint engine checks the measure
curl -X POST http://edge:8291/api/v1/actions/QC_PASS \
-H "Authorization: Bearer $TOKEN" -H "X-Idempotency-Key: $(uuidgen)" \
-H 'content-type: application/json' \
-d '{"payload":{"piece_id":"PZ-001","misura":0.018}}'
# misura 0.026 → the rule `tolleranza_collaudo` rejects: fail-closed, no commit.
// Node — the whole gesture, retry-safe
const r = await fetch(`${edge}/api/v1/actions/QC_PASS`, {
method: 'POST',
headers: { authorization: `Bearer ${token}`,
'x-idempotency-key': gestureKey, // REUSE the key on retry
'content-type': 'application/json' },
body: JSON.stringify({ payload }) });
if (r.status === 409) { /* FSM conflict — the aggregate is in another state */ }
if (r.status === 400 || r.status === 403) { /* read j.error — constraint or RBAC */ }
Known Failures
| Code | Meaning → corrective action |
|---|---|
409 ACTION_FAILED |
FSM transition not allowed from the current state → inspect the manifest transitions |
403 ACTION_FAILED |
Role lacks the trigger → manifest rbac_engine.permissions |
400 ACTION_FAILED |
A pre_commit_rule or validation failed → read error, resolve the blocker first |
409 IDEMPOTENCY_IN_PROGRESS |
Same key still in flight → back off and retry with the same key |
| Duplicate facts after retry | You changed the X-Idempotency-Key on retry — same gesture, same key |
401 AUTH_REQUIRED mid-session |
Re-login; do not retry the same dead token in a loop |