State Machines & Status Enums
Relevant source files
The following files were used as context for generating this wiki page:
State machines and status enums are central to the project's lifecycle management for entities such as leads, orders, and bookings. Rather than simple database flags, these systems govern complex "touchpoint" actions where a single state change triggers a cascade of atomic updates across multiple linked fields and external logs. This architecture ensures data consistency and provides reliable audit trails for downstream consumers like dashboards and cron jobs.
Effective state management in this project requires strict adherence to atomicity, where status updates are bundled with timestamps, activity types, and task management. Failure to synchronize these fields results in "silent data corruption" where the system state becomes inconsistent across different modules.
1. Atomic Touchpoint Pattern
A "touchpoint" is defined as any operation that updates an entity's lifecycle or status. To prevent linked-field drift, every function performing a touchpoint (e.g., apply_chip, mark_sent) must atomically update a canonical list of sibling fields within a single transaction.
Canonical Sibling Fields
When a status is updated, the following fields must typically be modified simultaneously:
| Field Name | Description | Requirement |
|---|---|---|
status | The primary lifecycle state (e.g., PROPOSAL_SENT, WON). | Must use valid Enum values. |
last_outbound_at | UTC timestamp of the latest outgoing communication. | Required for outbound touchpoints. |
last_activity_type | A string matching the ActivityType enum value. | Must match the Activity Log entry. |
tasks | Related open tasks in AUTO_CLOSE_TASK_TYPES. | Must be closed automatically. |
activity_log | A record of the intent and outcome of the transition. | Required for downstream tracking. |
Sources: claude-md-snippets/state-machine.md:3-11, BY-STACK/state-machine.md:14-26, bugbot-rules/linked-field-atomicity.md:7-12
Touchpoint Logic Flow
The following diagram illustrates the sequence of operations required for a valid state transition:
2. Transition Cascades and Dependencies
Certain state transitions carry mandatory requirements for related entities. Specifically, moving an entity into a "Booking" related state requires the existence of a corresponding row in the Booking table.
- Booking Requirements: Transitions to
BOOKING_PENDINGorBOOKEDmust be accompanied by aBookingrow; otherwise, aValidationErrormust be raised to prevent "orphan" states. - Terminal States: Transitions to final states like
WON,LOST, orARCHIVEDmust be performed through a dedicatedclose_lead()function rather than direct property assignment to ensure all cleanup logic is executed.
Sources: BY-STACK/state-machine.md:46-51, claude-md-snippets/state-machine.md:13-15
3. Activity Logging and Intent Tracking
The Activity Log serves as a source of truth for downstream processes. It is critical that logs record the intent of an action, even if the actual database update fails (e.g., due to a Compare-and-Swap failure or race condition).
Consistency Rules
- Intent Logs: If an update returns a row count of 0 (indicating the change was not applied), an activity log must still be written with
metadata.applied=false. - Type Matching: The value stored in the
last_activity_typecolumn on the entity must be identical to thetypestring written in the activity log. Discrepancies (e.g.,meeting_rejectedvsMEETING_CANCELED) break downstream filters.
Sources: BY-STACK/state-machine.md:58-69, claude-md-snippets/state-machine.md:17-21
4. Implementation Safeguards
Pydantic Schema Defaults
When using Pydantic models for API boundaries, default values can silently override caller intent. For example, a default of ContactChannel.WHATSAPP might incorrectly tag a lead coming from an email intake flow if the caller omits the field. Developers must ensure that defaults are only used when they are globally appropriate for all intake sources.
Sources: BY-STACK/state-machine.md:74-84, claude-md-snippets/state-machine.md:23-26
Multi-Role State Routing
In systems where users have multiple roles (e.g., both a driver and a secretary), state transitions must branch based on the target state or explicit context, not just the user's role. This prevents infinite routing loops where a "back" button defaults to a role-based menu rather than the previous state.
Sources: BY-STACK/state-machine.md:104-114, claude-md-snippets/state-machine.md:30-33
Compare-and-Swap (CAS) for Atomic Updates
To handle concurrent state changes, the project utilizes Atomic CAS. When updating a status, the query must include the expected current status in the WHERE clause.
-- Example CAS for status transition
UPDATE leads
SET status = :new_status, last_activity_type = :act_type
WHERE id = :id AND status = :expected_old_status;
Sources: claude-md-snippets/universal.md:3-5, BY-STACK/state-machine.md:61-64
Summary
The state machine architecture prioritizes Atomicity and Intent Visibility. By enforcing the synchronization of sibling fields (timestamps, logs, and status) and requiring intent-based logging even on failure, the system prevents inconsistent states that would otherwise disrupt cron jobs and management dashboards. Implementation must always account for cascading dependencies and avoid over-reliance on role-based logic or Pydantic defaults.