Skip to content

Approval routed to an empty position permanently locks the record (no admin override, no recovery) #3424

Description

@os-zhuang

Summary

When a record-change flow with an approval node routes to a position that has no holders, the approval request becomes permanently undecidable AND the underlying record stays RECORD_LOCKED forever, with no admin escape hatch. This is a data-availability dead-end: the record can never be edited or re-approved again.

Environment

  • @objectstack/* 16.1.0 (open edition), single-tenant, sqlite-wasm driver
  • Repro app: a CRM with opportunity_approval (a record_change flow, tiered approval nodes routed to { type: 'position', value: 'sales_manager' })

Steps to reproduce

  1. Define an approval node with lockRecord: true and approvers: [{ type: 'position', value: 'sales_manager' }].
  2. Ensure the sales_manager position exists but has zero sys_user_position rows (the default in a fresh org — seeds create positions but assign no holders).
  3. Trigger the flow (update a record so the entry condition matches).
  4. Observe the approval request is created with pending_approvers: ["position:sales_manager"] and the opportunity is locked.

Actual behaviour

  • POST /api/v1/approvals/requests/:id/approve as the org owner → 403 {"code":"FORBIDDEN","error":"actor '…' is not a pending approver"} — the owner is not in the (empty) resolved approver set.
  • POST …/:id/withdraw404 Not Found (no withdraw route for a flow-launched request).
  • PATCH /api/v1/data/crm_opportunity/:id400 {"error":"RECORD_LOCKED: record is locked while an approval is in progress"}.
  • Re-triggering the flow is suppressed by the in-flight re-entrancy guard, and the paused run’s mark_approved node fails with the same RECORD_LOCKED if resumed.
  • Net result: the request is stuck pending, the record is stuck locked, and the only way out is editing the sqlite DB directly (UPDATE sys_approval_request SET status='approved'; UPDATE crm_opportunity SET approval_status='approved' with the server stopped).

Expected behaviour

At least one of:

  1. Empty-approver detection at request creation — if a resolved approver set is empty, either auto-approve/auto-reject with a clear audit entry, or refuse to lock the record and surface a config error, rather than creating an undecidable request.
  2. Admin override / reassign — a privileged actor (org owner, modify_all_data, or a delegated approvals admin) should always be able to approve, reject, reassign, or withdraw a pending request and release the lock.
  3. Unlock on terminal failure — if the approval run dies (mark_approved throws RECORD_LOCKED), the lock should be released so the record isn’t orphaned.

Impact

High. Any approval routed to an unstaffed position permanently bricks the target record with no in-product recovery. This is very easy to hit in fresh/demo orgs (positions seeded, holders not) and in production whenever a role is vacated.

Workaround

Add a guaranteed-staffed fallback approver, e.g. { type: 'org_membership_level', value: 'owner' }, alongside the position approver so the set is never empty. This side-steps the dead-end but doesn’t address the missing override or the orphaned-lock-on-failure.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions