Workflow Customization
Every entity in Apostol CRM has a built-in state machine. The default 4-state lifecycle covers most cases, but you can extend it with custom states, methods, transitions, and events.
Default Workflow
Calling AddDefaultMethods(pClass) in an entity's init.sql creates this lifecycle:
┌────── restore ───────┐
v |
[Created] --enable--> [Enabled] --disable--> [Disabled]
| | |
|--disable--> [Disabled] |--enable--> [Enabled]
| | |
|--delete--> [Deleted] <--delete-- [Deleted] <--delete--
Four state types (fixed, defined by the Platform):
created-- initial state after object creationenabled-- active/operational statedisabled-- suspended/inactive statedeleted-- soft-deleted state
Ten global methods (available in any state):
create, open, edit, save, update, enable, disable, delete, restore, drop
Core Concepts
State Types vs States
A state type is one of the 4 fixed categories. A state is a specific named state that belongs to a state type. You can have multiple states per state type.
For example, a charging station entity has three states within the enabled state type:
enabled (state type)
├── available (state)
├── unavailable (state)
└── faulted (state)
Methods
A method binds an action to a class and (optionally) a state:
class-- which class this method belongs tostate-- which state the method is available in (NULL = global)action-- which action this method performsvisible-- whether it appears in the UI (default: true)
Transitions
A transition defines: "when in state X and method Y is executed, move to state Z."
Writing Custom Methods
Instead of AddDefaultMethods(pClass), write your own method registration. Here is a real example from a charging station entity:
CREATE OR REPLACE FUNCTION AddStationMethods (
pClass uuid
)
RETURNS void
AS $$
DECLARE
uState uuid;
rec_type record;
rec_state record;
rec_method record;
BEGIN
-- Step 1: Create global methods (state-independent)
PERFORM DefaultMethods(pClass);
-- Step 2: Create states and state-specific methods
FOR rec_type IN SELECT * FROM StateType
LOOP
CASE rec_type.code
WHEN 'created' THEN
uState := AddState(pClass, rec_type.id, rec_type.code, 'Created');
PERFORM AddMethod(null, pClass, uState, GetAction('enable'), null, 'Enable');
PERFORM AddMethod(null, pClass, uState, GetAction('delete'), null, 'Delete');
WHEN 'enabled' THEN
-- Multiple states within the 'enabled' state type:
uState := AddState(pClass, rec_type.id, 'available', 'Available');
PERFORM AddMethod(null, pClass, uState, GetAction('unavailable'),
null, 'Unavailable', null, false);
PERFORM AddMethod(null, pClass, uState, GetAction('faulted'),
null, 'Faulted', null, false);
PERFORM AddMethod(null, pClass, uState, GetAction('disable'),
null, 'Disable');
uState := AddState(pClass, rec_type.id, 'unavailable', 'Unavailable');
PERFORM AddMethod(null, pClass, uState, GetAction('available'),
null, 'Available', null, false);
PERFORM AddMethod(null, pClass, uState, GetAction('faulted'),
null, 'Faulted', null, false);
PERFORM AddMethod(null, pClass, uState, GetAction('disable'),
null, 'Disable');
uState := AddState(pClass, rec_type.id, 'faulted', 'Faulted');
PERFORM AddMethod(null, pClass, uState, GetAction('available'),
null, 'Available', null, false);
PERFORM AddMethod(null, pClass, uState, GetAction('disable'),
null, 'Disable');
WHEN 'disabled' THEN
uState := AddState(pClass, rec_type.id, rec_type.code, 'Disabled');
PERFORM AddMethod(null, pClass, uState, GetAction('enable'), null, 'Enable');
PERFORM AddMethod(null, pClass, uState, GetAction('delete'), null, 'Delete');
WHEN 'deleted' THEN
uState := AddState(pClass, rec_type.id, rec_type.code, 'Deleted');
PERFORM AddMethod(null, pClass, uState, GetAction('restore'), null, 'Restore');
PERFORM AddMethod(null, pClass, uState, GetAction('drop'), null, 'Drop');
END CASE;
END LOOP;
-- Step 3: Create default transitions for global methods
PERFORM DefaultTransition(pClass);
-- Step 4: Create custom transitions
FOR rec_state IN SELECT * FROM State WHERE class = pClass
LOOP
CASE rec_state.code
WHEN 'created' THEN
FOR rec_method IN SELECT * FROM Method WHERE state = rec_state.id
LOOP
IF rec_method.actioncode = 'enable' THEN
PERFORM AddTransition(rec_state.id, rec_method.id,
GetState(pClass, 'unavailable'));
END IF;
IF rec_method.actioncode = 'delete' THEN
PERFORM AddTransition(rec_state.id, rec_method.id,
GetState(pClass, 'deleted'));
END IF;
END LOOP;
WHEN 'available' THEN
FOR rec_method IN SELECT * FROM Method WHERE state = rec_state.id
LOOP
IF rec_method.actioncode = 'unavailable' THEN
PERFORM AddTransition(rec_state.id, rec_method.id,
GetState(pClass, 'unavailable'));
END IF;
IF rec_method.actioncode = 'faulted' THEN
PERFORM AddTransition(rec_state.id, rec_method.id,
GetState(pClass, 'faulted'));
END IF;
IF rec_method.actioncode = 'disable' THEN
PERFORM AddTransition(rec_state.id, rec_method.id,
GetState(pClass, 'disabled'));
END IF;
END LOOP;
-- ... similar for 'unavailable', 'faulted', 'disabled', 'deleted'
END CASE;
END LOOP;
END;
$$ LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = kernel, pg_temp;
This creates the following state machine:
┌─── heartbeat (self) ───┐
v |
[Created] --enable--> [Unavailable] --available--> [Available]
| | |
|--delete--> |--faulted--> [Faulted] |
| |--disable--> | |--faulted-->
| | |--disable-->
v v v
[Deleted] <--delete-- [Disabled] <--disable--
Key API Functions
AddState
uState := AddState(pClass, pStateType, pCode, pLabel);
pClass-- class UUIDpStateType-- state type UUID (fromGetStateType('enabled')or loop variable)pCode-- unique state code within the class (e.g.,'available')pLabel-- display label
AddMethod
uMethod := AddMethod(pId, pClass, pState, pAction, pCode, pLabel, pSequence, pVisible);
pState-- state UUID (NULL = global method, available in any state)pAction-- action UUID fromGetAction('...')pVisible-- whether to show in UI (default: true, set tofalsefor automated methods)
AddTransition
PERFORM AddTransition(pCurrentState, pMethod, pNewState);
Defines: when in pCurrentState and pMethod is executed, move to pNewState.
Method Visibility
Methods can be visible or hidden:
-- Hidden: triggered by the system, not the user
PERFORM AddMethod(null, pClass, uState, GetAction('heartbeat'),
null, 'Heartbeat', null, false);
-- Visible: user can trigger it from the UI
PERFORM AddMethod(null, pClass, uState, GetAction('disable'),
null, 'Disable');
Use hidden methods for automated state changes (heartbeat, status updates from external systems, background processes).
Actions Without State Change
Some actions execute their event handlers without changing state. No AddTransition call is needed:
WHEN 'enabled' THEN
uState := AddState(pClass, rec_type.id, rec_type.code, 'Enabled');
-- These actions stay in the same state:
PERFORM AddMethod(null, pClass, uState, GetAction('submit'), pVisible => false);
PERFORM AddMethod(null, pClass, uState, GetAction('confirm'), pVisible => false);
PERFORM AddMethod(null, pClass, uState, GetAction('reconfirm'));
Custom Event Handlers
When you add custom actions, you also need events and event handlers. In your Add<Entity>Events function:
IF r.code = 'available' THEN
PERFORM AddEvent(pClass, uEvent, r.id, 'Station available',
'EventStationAvailable();');
PERFORM AddEvent(pClass, uEvent, r.id, 'Change state',
'ChangeObjectState();');
END IF;
The ChangeObjectState() call is important for custom actions that are not part of the standard lifecycle -- it triggers the actual state transition. Standard actions (enable, disable, delete, restore) handle state changes automatically.
Design Patterns
| Pattern | When to Use | Example |
|---|---|---|
AddDefaultMethods(pClass) | Simple CRUD with standard lifecycle | Region, Currency |
| Custom methods, 1 state per type | Standard lifecycle with extra actions | Client (submit/confirm) |
| Custom methods, N states per type | Complex lifecycle with sub-states | Station (available/unavailable/faulted) |
| Hidden methods | Automated/system-triggered actions | Heartbeat, status sync |
| No-transition actions | Actions that execute events only | Submit, confirm |