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 creation
  • enabled -- active/operational state
  • disabled -- suspended/inactive state
  • deleted -- 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 to
  • state -- which state the method is available in (NULL = global)
  • action -- which action this method performs
  • visible -- 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 UUID
  • pStateType -- state type UUID (from GetStateType('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 from GetAction('...')
  • pVisible -- whether to show in UI (default: true, set to false for 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

PatternWhen to UseExample
AddDefaultMethods(pClass)Simple CRUD with standard lifecycleRegion, Currency
Custom methods, 1 state per typeStandard lifecycle with extra actionsClient (submit/confirm)
Custom methods, N states per typeComplex lifecycle with sub-statesStation (available/unavailable/faulted)
Hidden methodsAutomated/system-triggered actionsHeartbeat, status sync
No-transition actionsActions that execute events onlySubmit, confirm