Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 

README.md

@objectql/plugin-workflow

State machine workflow engine plugin for ObjectQL. Provides full XState-level state machine execution with guards, actions, and compound states.

Overview

The Workflow Plugin implements a powerful state machine engine that manages complex business workflows through declarative configuration. It operates at the Hook/Validation layer, intercepting state field changes via beforeUpdate hooks to evaluate guards, execute actions, and enforce state transition rules.

Key Features

  • XState-Compatible: Full support for XState state machine patterns
  • Guard Conditions: Declarative conditions that control transitions
  • Entry/Exit Actions: Execute side effects during state transitions
  • Compound States: Hierarchical state nesting with automatic resolution
  • Audit Trail: Optional persistence of all state transitions
  • Type-Safe: Full TypeScript support with protocol-derived types
  • Zero SQL Impact: Operates at hook layer, no changes to query generation

Installation

pnpm add @objectql/plugin-workflow

Quick Start

1. Define State Machine in Object Metadata

# project.object.yml
name: project
fields:
  status:
    type: select
    options: [draft, active, done]
    default: draft

stateMachine:
  initial: draft
  states:
    draft:
      on:
        submit:
          target: active
    active:
      on:
        complete:
          target: done
    done:
      type: final

2. Install the Plugin

import { WorkflowPlugin } from '@objectql/plugin-workflow';
import { ObjectStackKernel } from '@objectstack/runtime';

const kernel = new ObjectStackKernel([
  new WorkflowPlugin({
    enableAuditTrail: true,
  }),
  // ... other plugins
]);

await kernel.start();

3. State Transitions Work Automatically

// Valid transition - allowed
await api.update('project', 'p1', { status: 'active' });
// ✅ Success

// Invalid transition - denied
await api.update('project', 'p1', { status: 'done' });
// ❌ ObjectQLError: TRANSITION_DENIED

Features

Guards (Conditions)

Control which transitions are allowed:

stateMachine:
  states:
    draft:
      on:
        submit:
          target: pending_approval
          cond:
            field: complete
            operator: equals
            value: true

Built-in guards:

  • hasRole:roleName - User has specific role
  • hasPermission:permission - User has permission
  • isOwner - User owns the record
  • isCreator - User created the record
  • field:expression - Field-based conditions

Actions

Execute side effects during transitions:

stateMachine:
  states:
    draft:
      exit:
        - timestamp:submitted_at
    active:
      entry:
        - notifyStakeholders
        - setField:active_at=$now

Built-in actions:

  • setField:field=value - Update field value
  • increment:field - Increment numeric field
  • timestamp:field - Set timestamp
  • log:message - Console log

Compound States

Hierarchical state nesting:

stateMachine:
  initial: editing
  states:
    editing:
      initial: draft
      states:
        draft: {}
        review: {}
      on:
        publish:
          target: published
    published:
      type: final

Audit Trail

Track all state transitions:

const plugin = new WorkflowPlugin({ enableAuditTrail: true });

// Query audit trail
const trail = plugin.getAuditTrail({
  objectName: 'project',
  recordId: 'p123',
});

console.log(trail);
// [
//   {
//     id: '...',
//     objectName: 'project',
//     recordId: 'p123',
//     currentState: 'active',
//     previousState: 'draft',
//     timestamp: '2026-02-07T...',
//     actionsExecuted: ['onExitDraft', 'onEnterActive'],
//   }
// ]

Configuration

WorkflowPluginConfig

interface WorkflowPluginConfig {
  /** Enable audit trail persistence. Default: false */
  enableAuditTrail?: boolean;

  /** Custom guard resolver for external guards */
  guardResolver?: (guardRef: string, context: ExecutionContext) => Promise<boolean>;

  /** Custom action executor for external actions */
  actionExecutor?: (actionRef: string, context: ExecutionContext) => Promise<void>;
}

Custom Guard Resolver

const plugin = new WorkflowPlugin({
  guardResolver: async (guardRef, context) => {
    if (guardRef === 'budgetApproved') {
      const budget = await fetchBudgetStatus(context.record.id);
      return budget.approved;
    }
    return false;
  },
});

Custom Action Executor

const plugin = new WorkflowPlugin({
  actionExecutor: async (actionRef, context) => {
    if (actionRef === 'notifyApprover') {
      await sendEmail({
        to: context.record.approver_email,
        subject: 'Approval Required',
        body: `Project ${context.record.name} needs your approval`,
      });
    }
  },
});

API Reference

StateMachineEngine

import { StateMachineEngine } from '@objectql/plugin-workflow';

const engine = new StateMachineEngine(config, guardEvaluator, actionExecutor);

const result = await engine.transition('draft', 'active', context);

if (result.allowed) {
  console.log('Transition allowed');
} else {
  console.error('Denied:', result.error);
}

GuardEvaluator

import { GuardEvaluator } from '@objectql/plugin-workflow';

const evaluator = new GuardEvaluator(customResolver);

const result = await evaluator.evaluate(
  { field: 'approved', operator: 'equals', value: true },
  context
);

console.log(result.passed); // true or false

ActionExecutor

import { ActionExecutor } from '@objectql/plugin-workflow';

const executor = new ActionExecutor(customExecutor);

await executor.execute('notifyApprover', context);
await executor.executeMultiple(['action1', 'action2'], context);

Architecture

┌──────────────────────────────┐
│  plugin-workflow             │  ← beforeUpdate hook
│  (State Machine Executor)    │
├──────────────────────────────┤
│  plugin-validator            │  ← field/cross-field validation
├──────────────────────────────┤
│  QueryService → QueryAST     │  ← Core: query building
├──────────────────────────────┤
│  Driver → Knex → SQL         │  ← Driver: SQL generation (UNTOUCHED)
└──────────────────────────────┘

The Workflow Plugin:

  1. Registers a beforeUpdate hook
  2. Detects state field changes in the update payload
  3. Evaluates guards against the state machine configuration
  4. Executes entry/exit/transition actions
  5. Either allows the update to proceed or throws ObjectQLError

Examples

Approval Workflow

name: expense_report
stateMachine:
  initial: draft
  states:
    draft:
      on:
        submit:
          target: pending
          cond:
            field: amount
            operator: greater_than
            value: 0
    
    pending:
      entry:
        - notifyApprover
      on:
        approve:
          target: approved
          cond: hasRole:approver
          actions:
            - setField:approved_by=$user.id
            - timestamp:approved_at
        reject:
          target: rejected
    
    approved:
      type: final
    
    rejected:
      on:
        resubmit:
          target: draft

Project Lifecycle

name: project
stateMachine:
  initial: planning
  states:
    planning:
      on:
        start:
          target: active
          cond:
            all_of:
              - field: team_assigned
                operator: equals
                value: true
              - field: budget_approved
                operator: equals
                value: true
    
    active:
      entry:
        - setField:started_at=$now
        - notifyTeam
      on:
        pause:
          target: on_hold
        complete:
          target: completed
    
    on_hold:
      on:
        resume:
          target: active
    
    completed:
      type: final

Testing

import { describe, it, expect } from 'vitest';
import { WorkflowPlugin } from '@objectql/plugin-workflow';

describe('Project Workflow', () => {
  it('should allow valid transitions', async () => {
    const plugin = new WorkflowPlugin({ enableAuditTrail: true });
    
    // Test setup...
    
    const result = await engine.transition('draft', 'active', context);
    expect(result.allowed).toBe(true);
  });
});

Run tests:

pnpm test

License

MIT

Contributing

See the main ObjectQL Contributing Guide.

Documentation

Full documentation: ObjectQL Workflow Engine