No workflow can start once the system has an account

Table of Contents

This page is a capture in the inbox bucket of the product backlog — a pre-sprint idea, not yet pulled into a sprint as a story.

1. What

Fixed on 2026-09-26. The engine now sets modified_by at all four creation sites – the instance and the three step kinds (step 0, the next step, and a compensation step) – and the hand-written foreign key that stopped the database recreating at all is dropped, since the steps insert trigger already enforces the relationship as a soft foreign key. Verified against a recreated database: it completes with no workaround, 75 workflow instances and 183 steps are created, and iam's provisioning command returns success:true end to end instead of timing out. The attribution this leaves behind – the service account rather than the caller – is its own capture. Delete this page once the fix has merged.

ores.workflow's engine builds its workflow_instance and its first workflow_step without setting modified_by, and the generated repository writes what it is given. Every table's insert trigger routes that field through ores_iam_validate_account_username_fn(), which rejects an empty value with 23502 once the tenant holds a non-service account:

Failed to create workflow instance 971da194-0fe1-4e37-a415-c70d947b7d9c:
Repository error: Executing INSERT failed: ERROR:  modified_by cannot be null
or empty. Application code must set this field explicitly.

The fix is one field on two entities, the same two the generators already set: r.modified_by alongside r.performed_by. performed_by needs no application value – the trigger stamps it from the current service.

2. Why

A workflow that cannot start is a silent failure with a misleading symptom. The engine logs the failure and publishes a status event, but a caller that dispatches and then waits cannot tell it apart from a slow service: iam's internal_request_client::wait_for_workflow_instance() polls workflow.v1.instances.steps every 500ms, treats "Workflow instance not found" as transient – the code comments expect a busy workflow service to go unresponsive for a whole run – and keeps polling until its 1500-second deadline. The client's own request timeout fires first, so the operator sees NATS request timed out and never the reason. The iam tenant provisioning recipe reproduces this in one command: tenant_provisioning provision-acme-tenant.

Why it survived: the resolver defaults an empty modified_by to current_user while the tenant has no user accounts, so a bare or pre-bootstrap database accepts the insert. The defect therefore appears only once an administrator exists – the normal state of any real system, and never the state a fresh test database is in.

Both workflow tables are affected: workflow_workflow_instances_tbl and workflow_workflow_steps_tbl carry modified_by text not null and the same trigger.

3. References

  • Found on 2026-09-26 chasing a V04 timeout on Bring ores.iam to the clean standard; the full trace, the live polls and the database counts are on that task's V04 row.
  • projects/ores.workflow/core/src/service/workflow_engine.cpp — on_start_workflow(), where the instance and step are constructed.
  • projects/ores.sql/create/iam/iam_tenant_functions_create.sql — ores_iam_validate_account_username_fn(), the bootstrap branch and the 23502 raise.
  • projects/ores.iam/core/src/service/internal_request_client.cpp — the wait that cannot distinguish a failed start from a slow service.

4. See also

  • Component Clean Standard — W01 is the same shape of defect one layer up: a family that exists but is never composed, and no gate that can see it.

Emacs 29.3 (Org mode 9.6.15)