Volume 3: Human-System Collaboration

Pattern 17: State-Aware Behavior

Part II: Interaction Patterns - Temporal Patterns


Opening Scenario: The Form That Changed Its Personality

Monica was reviewing expense reports in the company's expense system. She opened Report #3847, submitted by David three days ago.

The form looked identical to when David had created it:

Expense Report #3847

Amount: $1,247.38    [Editable text field]
Category: [Travel ▼]  [Editable dropdown]
Description: [       ] [Editable textarea]

[Submit] [Delete]

Monica could edit everything. She accidentally changed the amount from $1,247.38 to $1,247.83. She saved it.

David got a notification: "Your expense report has been modified."

He looked at his report. "I submitted $1,247.38. Why does it now show $1,247.83? Did I make a mistake?"

No - Monica had accidentally changed it while reviewing. But the form treated her review session the same as David's creation session.


The IT director, Susan, investigated. The form had no concept of "state":

// Bad: Same behavior regardless of state
function renderExpenseForm(report) {
  return `
    <input name="amount" value="${report.amount}">
    <select name="category">...</select>
    <textarea name="description">${report.description}</textarea>

    <button>Submit</button>
    <button>Delete</button>
  `;
}

// Creator, reviewer, accountant - everyone sees the same form
// No awareness of:
// - Who is viewing
// - What state the report is in
// - What actions are appropriate

Susan mapped out the actual workflow:

Draft → Submitted → Under Review → Approved → Paid
                         ↓
                    Rejected → Draft (resubmit)

Each state should have different behavior:

Draft State (creator only): - All fields editable - Can save without submitting - Can delete draft - "Submit for Review" button

Submitted State (creator can view, not edit): - All fields read-only for creator - "Withdraw" button (return to draft) - Status: "Waiting for review"

Under Review (reviewer only): - Amount/Category read-only (preserve creator's data) - Can add reviewer notes - "Approve" or "Reject" buttons - "Request Changes" option

Approved State (read-only for all): - All fields locked - Shows approval timestamp and approver - "Export to PDF" option - Waiting for payment processing

Rejected State (creator can revise): - Rejection reason shown prominently - Creator can edit and resubmit - Original values preserved for reference


Susan built a state-aware system:

class StateAwareForm {
  constructor(document, currentUser) {
    this.document = document;
    this.currentUser = currentUser;
    this.state = document.state;
  }

  render() {
    const stateConfig = this.getStateConfiguration();

    return this.renderWithConfiguration(stateConfig);
  }

  getStateConfiguration() {
    const configs = {
      'draft': {
        editable: ['amount', 'category', 'description', 'receipts'],
        visible: ['all'],
        actions: ['save', 'submit', 'delete'],
        restrictions: [],
        message: 'Complete your expense report'
      },

      'submitted': {
        editable: [],  // Nothing editable
        visible: ['all'],
        actions: ['withdraw'],
        restrictions: ['no-edit'],
        message: 'Submitted - Waiting for review',
        canView: ['creator', 'reviewer', 'accountant']
      },

      'under_review': {
        editable: ['reviewer_notes', 'reviewer_category_adjustment'],
        visible: ['all'],
        actions: ['approve', 'reject', 'request_changes'],
        restrictions: ['creator-locked'],
        message: 'Under review by ' + this.document.reviewer,
        canEdit: ['reviewer']
      },

      'approved': {
        editable: [],
        visible: ['all'],
        actions: ['export_pdf'],
        restrictions: ['fully-locked'],
        message: 'Approved on ' + this.document.approvalDate,
        badge: 'success'
      },

      'rejected': {
        editable: ['amount', 'category', 'description', 'receipts'],
        visible: ['all', 'rejection_reason'],
        actions: ['resubmit'],
        restrictions: [],
        message: 'Rejected - See feedback below',
        badge: 'error'
      }
    };

    return configs[this.state];
  }
}

Now when Monica reviewed a submitted expense:

Expense Report #3847  [UNDER REVIEW]

Amount: $1,247.38    [Read-only, locked 🔒]
Category: Travel     [Read-only, locked 🔒]
Description: ...     [Read-only, locked 🔒]

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
REVIEWER SECTION

Reviewer Notes: [                          ]
                 Your comments for the submitter

Category Adjustment: [Travel ▼]
                      Correct category if needed

[Approve] [Reject] [Request Changes]

Monica couldn't accidentally edit David's data. The form knew its state and adapted its behavior.

When David viewed his submitted report:

Expense Report #3847  [SUBMITTED]

Status: Waiting for Monica's review
Submitted: Dec 12, 2024 at 3:47 PM

Amount: $1,247.38    [Your submission 👁️]
Category: Travel
Description: ...

[Withdraw Submission]

Everything read-only for him, but he could withdraw if needed.

After approval:

Expense Report #3847  [✓ APPROVED]

Approved by: Monica Chen
Approved on: Dec 15, 2024 at 11:23 AM

Amount: $1,247.38    [Approved ✓]
Category: Travel     [Approved ✓]
Description: ...     [Approved ✓]

Payment Status: Processing (ETA: Dec 20)

[Export PDF] [Print]

Everything locked. Clear status. Appropriate actions only.

Accidental edits: Zero. User confusion: Eliminated. Workflow clarity: Perfect.

Context

State-Aware Behavior applies when:

Documents have lifecycle: Draft → Submitted → Approved → Archived

Permissions vary by state: Different users can do different things at different stages

Validation changes: Rules differ between draft and final submission

UI should adapt: What's editable, visible, and actionable changes

Workflow has branches: Approval, rejection, revision paths

Audit requirements: Track who did what in which state

User roles matter: Creator, reviewer, approver have different capabilities

Problem Statement

Most forms ignore workflow state, treating all interactions identically:

State-blind rendering:

// Bad: Same form regardless of state
function renderForm(data) {
  return `
    <input name="field1" value="${data.field1}">
    <input name="field2" value="${data.field2}">
    <button>Submit</button>
  `;
}

// Draft? Submitted? Approved?
// Form doesn't know, doesn't care

No permission enforcement:

// Bad: Anyone can edit anything anytime
app.post('/update', (req, res) => {
  document.update(req.body);
  // No check of:
  // - Document state
  // - User permissions
  // - Allowed actions
});

Inappropriate actions available:

<!-- Bad: Delete button on approved documents -->
<form>
  <input name="amount" value="$5000">

  <button>Save</button>
  <button>Submit</button>
  <button>Delete</button>  <!-- Approved docs shouldn't be deletable! -->
</form>

No state indication:

<!-- Bad: No visual indication of state -->
<h1>Expense Report #3847</h1>
<input name="amount" value="$1247.38">

<!-- Is this:
  - A draft I'm creating?
  - A submitted report I'm reviewing?
  - An approved report I'm viewing?
  User can't tell! -->

State transitions uncontrolled:

// Bad: Any state can become any other state
document.state = newState;  // No validation
// Can go from Draft directly to Paid?
// Can go from Approved back to Draft?
// No workflow enforcement

We need forms that understand their state, adapt their behavior accordingly, and enforce proper workflow transitions.

Forces

Flexibility vs Control

  • Users want flexibility to edit
  • But state requires restrictions
  • Balance freedom with workflow integrity

Simplicity vs State Complexity

  • Simple forms are easy
  • But workflows are complex
  • Balance ease-of-use with capability

Permissions vs Usability

  • Strict permissions prevent errors
  • But can frustrate legitimate actions
  • Balance security with efficiency

Transparency vs Clutter

  • State should be obvious
  • But too many indicators overwhelm
  • Balance clarity with cleanliness

Consistency vs Context

  • Consistent UI is learnable
  • But context requires adaptation
  • Balance familiarity with appropriateness

Solution

Implement state machine that defines document lifecycle, configures form behavior per state, enforces transition rules, and provides clear visual indication of current state and available actions.

The pattern has six key strategies:

1. State Machine Definition

Define all possible states and transitions:

class DocumentStateMachine {
  constructor(documentType) {
    this.documentType = documentType;
    this.states = this.defineStates();
    this.transitions = this.defineTransitions();
  }

  defineStates() {
    return {
      'draft': {
        name: 'Draft',
        description: 'Being created or edited',
        color: '#95a5a6',
        icon: '✏️',
        terminal: false
      },

      'submitted': {
        name: 'Submitted',
        description: 'Awaiting review',
        color: '#3498db',
        icon: '📤',
        terminal: false
      },

      'under_review': {
        name: 'Under Review',
        description: 'Being reviewed by approver',
        color: '#f39c12',
        icon: '👁️',
        terminal: false
      },

      'approved': {
        name: 'Approved',
        description: 'Approved for processing',
        color: '#27ae60',
        icon: '✓',
        terminal: false
      },

      'rejected': {
        name: 'Rejected',
        description: 'Returned for revision',
        color: '#e74c3c',
        icon: '✗',
        terminal: false
      },

      'paid': {
        name: 'Paid',
        description: 'Payment completed',
        color: '#16a085',
        icon: '💰',
        terminal: true  // Final state
      },

      'cancelled': {
        name: 'Cancelled',
        description: 'Cancelled by creator',
        color: '#7f8c8d',
        icon: '🚫',
        terminal: true  // Final state
      }
    };
  }

  defineTransitions() {
    return [
      // From Draft
      { from: 'draft', to: 'submitted', action: 'submit', 
        requires: ['complete-data'], actor: 'creator' },
      { from: 'draft', to: 'cancelled', action: 'cancel', 
        actor: 'creator' },

      // From Submitted
      { from: 'submitted', to: 'under_review', action: 'assign_reviewer',
        actor: 'manager' },
      { from: 'submitted', to: 'draft', action: 'withdraw',
        actor: 'creator' },

      // From Under Review
      { from: 'under_review', to: 'approved', action: 'approve',
        requires: ['reviewer-notes'], actor: 'reviewer' },
      { from: 'under_review', to: 'rejected', action: 'reject',
        requires: ['rejection-reason'], actor: 'reviewer' },
      { from: 'under_review', to: 'submitted', action: 'request_changes',
        requires: ['change-request'], actor: 'reviewer' },

      // From Rejected
      { from: 'rejected', to: 'submitted', action: 'resubmit',
        requires: ['addressed-feedback'], actor: 'creator' },
      { from: 'rejected', to: 'cancelled', action: 'cancel',
        actor: 'creator' },

      // From Approved
      { from: 'approved', to: 'paid', action: 'process_payment',
        actor: 'accountant' }
    ];
  }

  canTransition(fromState, toState, actor, document) {
    // Find the transition
    const transition = this.transitions.find(t =>
      t.from === fromState && t.to === toState
    );

    if (!transition) {
      return {
        allowed: false,
        reason: `No transition from ${fromState} to ${toState}`
      };
    }

    // Check actor permission
    if (transition.actor && !this.hasRole(actor, transition.actor)) {
      return {
        allowed: false,
        reason: `Only ${transition.actor} can perform this action`
      };
    }

    // Check requirements
    if (transition.requires) {
      for (const requirement of transition.requires) {
        if (!this.checkRequirement(requirement, document)) {
          return {
            allowed: false,
            reason: `Missing: ${requirement}`
          };
        }
      }
    }

    return { allowed: true };
  }

  getAvailableTransitions(currentState, actor, document) {
    return this.transitions
      .filter(t => t.from === currentState)
      .filter(t => {
        const check = this.canTransition(currentState, t.to, actor, document);
        return check.allowed;
      })
      .map(t => ({
        action: t.action,
        toState: t.to,
        label: this.getActionLabel(t.action),
        requires: t.requires || []
      }));
  }

  getActionLabel(action) {
    const labels = {
      'submit': 'Submit for Review',
      'withdraw': 'Withdraw Submission',
      'approve': 'Approve',
      'reject': 'Reject',
      'resubmit': 'Resubmit',
      'cancel': 'Cancel',
      'process_payment': 'Process Payment'
    };

    return labels[action] || action;
  }
}

2. State-Based Configuration

Configure form behavior per state:

class StateConfiguration {
  constructor(stateMachine) {
    this.stateMachine = stateMachine;
    this.configs = this.defineConfigurations();
  }

  defineConfigurations() {
    return {
      'draft': {
        fields: {
          editable: ['all'],
          required: ['amount', 'category'],
          visible: ['all'],
          readonly: []
        },

        validation: {
          strictness: 'lenient',  // Allow saving incomplete
          showWarnings: true,
          blockSubmit: ['amount', 'category', 'description']
        },

        actions: {
          available: ['save', 'submit', 'cancel'],
          primary: 'save',
          destructive: 'cancel'
        },

        ui: {
          showAutosave: true,
          showProgress: true,
          allowAttachments: true,
          showDraftIndicator: true
        }
      },

      'submitted': {
        fields: {
          editable: [],
          required: [],
          visible: ['all'],
          readonly: ['all']
        },

        validation: {
          strictness: 'strict',
          showWarnings: false,
          blockSubmit: []
        },

        actions: {
          available: ['withdraw', 'export'],
          primary: null,
          destructive: 'withdraw'
        },

        ui: {
          showAutosave: false,
          showProgress: false,
          allowAttachments: false,
          showSubmissionInfo: true
        }
      },

      'under_review': {
        fields: {
          editable: ['reviewer_notes', 'reviewer_adjustments'],
          required: [],
          visible: ['all', 'reviewer_fields'],
          readonly: ['creator_fields']
        },

        validation: {
          strictness: 'strict',
          showWarnings: true,
          blockSubmit: ['reviewer_notes']  // Required for approve/reject
        },

        actions: {
          available: ['approve', 'reject', 'request_changes'],
          primary: 'approve',
          destructive: 'reject'
        },

        ui: {
          showAutosave: true,
          showProgress: false,
          allowAttachments: true,
          highlightReviewerSection: true
        }
      },

      'approved': {
        fields: {
          editable: [],
          required: [],
          visible: ['all', 'approval_info'],
          readonly: ['all']
        },

        validation: {
          strictness: 'none',  // No editing possible
          showWarnings: false,
          blockSubmit: []
        },

        actions: {
          available: ['export', 'print'],
          primary: 'export',
          destructive: null
        },

        ui: {
          showAutosave: false,
          showProgress: false,
          allowAttachments: false,
          showApprovalBanner: true
        }
      },

      'rejected': {
        fields: {
          editable: ['all'],
          required: ['amount', 'category', 'description'],
          visible: ['all', 'rejection_info'],
          readonly: []
        },

        validation: {
          strictness: 'strict',
          showWarnings: true,
          blockSubmit: ['amount', 'category', 'description']
        },

        actions: {
          available: ['save', 'resubmit', 'cancel'],
          primary: 'resubmit',
          destructive: 'cancel'
        },

        ui: {
          showAutosave: true,
          showProgress: true,
          allowAttachments: true,
          showRejectionFeedback: true,
          highlightChangesNeeded: true
        }
      }
    };
  }

  getConfiguration(state) {
    return this.configs[state] || this.configs['draft'];
  }

  isFieldEditable(fieldName, state) {
    const config = this.getConfiguration(state);

    if (config.fields.editable.includes('all')) {
      return !config.fields.readonly.includes(fieldName);
    }

    return config.fields.editable.includes(fieldName);
  }

  isFieldVisible(fieldName, state) {
    const config = this.getConfiguration(state);

    if (config.fields.visible.includes('all')) {
      return true;
    }

    return config.fields.visible.includes(fieldName);
  }

  getAvailableActions(state) {
    const config = this.getConfiguration(state);
    return config.actions.available;
  }
}

3. Permission-Aware Rendering

Render based on state and user role:

class StateAwareRenderer {
  constructor(stateMachine, stateConfig) {
    this.stateMachine = stateMachine;
    this.stateConfig = stateConfig;
  }

  render(document, currentUser) {
    const state = document.state;
    const config = this.stateConfig.getConfiguration(state);

    return `
      ${this.renderStateIndicator(state)}
      ${this.renderFields(document, state, currentUser)}
      ${this.renderActions(document, state, currentUser)}
      ${this.renderStateInfo(document, state)}
    `;
  }

  renderStateIndicator(state) {
    const stateInfo = this.stateMachine.states[state];

    return `
      <div class="state-indicator state-${state}">
        <span class="state-icon">${stateInfo.icon}</span>
        <span class="state-name">${stateInfo.name}</span>
        <span class="state-description">${stateInfo.description}</span>
      </div>
    `;
  }

  renderFields(document, state, currentUser) {
    const config = this.stateConfig.getConfiguration(state);
    let html = '<div class="document-fields">';

    // Render each field based on configuration
    Object.keys(document.data).forEach(fieldName => {
      if (this.stateConfig.isFieldVisible(fieldName, state)) {
        const editable = this.stateConfig.isFieldEditable(fieldName, state);
        const value = document.data[fieldName];

        html += this.renderField(fieldName, value, editable, state);
      }
    });

    // Add state-specific fields
    if (state === 'under_review') {
      html += this.renderReviewerFields(document);
    }

    if (state === 'rejected') {
      html += this.renderRejectionInfo(document);
    }

    if (state === 'approved') {
      html += this.renderApprovalInfo(document);
    }

    html += '</div>';
    return html;
  }

  renderField(fieldName, value, editable, state) {
    if (editable) {
      return `
        <div class="field editable">
          <label>${this.getFieldLabel(fieldName)}</label>
          <input name="${fieldName}" value="${value}">
          <span class="field-hint">Editable in ${state} state</span>
        </div>
      `;
    } else {
      return `
        <div class="field readonly">
          <label>${this.getFieldLabel(fieldName)}</label>
          <div class="field-value">
            ${value}
            <span class="lock-icon">🔒</span>
          </div>
          <span class="field-hint">Locked in ${state} state</span>
        </div>
      `;
    }
  }

  renderActions(document, state, currentUser) {
    const availableTransitions = this.stateMachine.getAvailableTransitions(
      state,
      currentUser,
      document
    );

    if (availableTransitions.length === 0) {
      return '';
    }

    const config = this.stateConfig.getConfiguration(state);

    let html = '<div class="document-actions">';

    availableTransitions.forEach(transition => {
      const isPrimary = transition.action === config.actions.primary;
      const isDestructive = transition.action === config.actions.destructive;

      let buttonClass = 'action-button';
      if (isPrimary) buttonClass += ' primary';
      if (isDestructive) buttonClass += ' destructive';

      html += `
        <button 
          class="${buttonClass}"
          data-action="${transition.action}"
          data-to-state="${transition.toState}"
        >
          ${transition.label}
        </button>
      `;
    });

    html += '</div>';
    return html;
  }

  renderStateInfo(document, state) {
    let html = '<div class="state-info">';

    if (state === 'submitted') {
      html += `
        <p>Submitted on ${document.submittedDate}</p>
        <p>Waiting for review assignment</p>
      `;
    }

    if (state === 'under_review') {
      html += `
        <p>Assigned to: ${document.reviewer}</p>
        <p>Review started: ${document.reviewStartDate}</p>
      `;
    }

    if (state === 'approved') {
      html += `
        <p>Approved by: ${document.approver}</p>
        <p>Approved on: ${document.approvalDate}</p>
        <p>Next: Payment processing</p>
      `;
    }

    html += '</div>';
    return html;
  }

  renderReviewerFields(document) {
    return `
      <div class="reviewer-section">
        <h3>Reviewer Section</h3>

        <div class="field editable">
          <label>Reviewer Notes</label>
          <textarea name="reviewer_notes" rows="4"></textarea>
          <span class="field-hint">Your comments for the submitter</span>
        </div>

        <div class="field editable">
          <label>Category Adjustment</label>
          <select name="reviewer_category">
            <option>Keep as submitted</option>
            <option>Travel</option>
            <option>Meals</option>
            <option>Equipment</option>
          </select>
        </div>
      </div>
    `;
  }

  renderRejectionInfo(document) {
    return `
      <div class="rejection-feedback">
        <h3>Rejection Feedback</h3>
        <div class="feedback-box">
          <p><strong>Reason:</strong></p>
          <p>${document.rejectionReason}</p>

          <p><strong>Requested Changes:</strong></p>
          <p>${document.changesRequested}</p>
        </div>
      </div>
    `;
  }

  renderApprovalInfo(document) {
    return `
      <div class="approval-info success">
        <h3>✓ Approval Details</h3>
        <dl>
          <dt>Approved By:</dt>
          <dd>${document.approver}</dd>

          <dt>Approval Date:</dt>
          <dd>${document.approvalDate}</dd>

          <dt>Approval Notes:</dt>
          <dd>${document.approvalNotes || 'None'}</dd>
        </dl>
      </div>
    `;
  }
}

4. State Transition Validation

Ensure only valid transitions occur:

class StateTransitionValidator {
  constructor(stateMachine) {
    this.stateMachine = stateMachine;
  }

  validateTransition(document, toState, actor, additionalData = {}) {
    const currentState = document.state;

    // Can this transition happen?
    const canTransition = this.stateMachine.canTransition(
      currentState,
      toState,
      actor,
      document
    );

    if (!canTransition.allowed) {
      return {
        valid: false,
        errors: [canTransition.reason]
      };
    }

    // Get the transition
    const transition = this.stateMachine.transitions.find(t =>
      t.from === currentState && t.to === toState
    );

    // Validate requirements
    const errors = [];

    if (transition.requires) {
      transition.requires.forEach(requirement => {
        if (!this.validateRequirement(requirement, document, additionalData)) {
          errors.push(`Missing: ${requirement}`);
        }
      });
    }

    // State-specific validation
    const stateValidation = this.validateStateSpecificRules(
      currentState,
      toState,
      document,
      additionalData
    );

    if (!stateValidation.valid) {
      errors.push(...stateValidation.errors);
    }

    return {
      valid: errors.length === 0,
      errors: errors,
      transition: transition
    };
  }

  validateRequirement(requirement, document, additionalData) {
    switch(requirement) {
      case 'complete-data':
        return document.amount && 
               document.category && 
               document.description;

      case 'reviewer-notes':
        return additionalData.reviewer_notes && 
               additionalData.reviewer_notes.length > 10;

      case 'rejection-reason':
        return additionalData.rejection_reason && 
               additionalData.rejection_reason.length > 20;

      case 'addressed-feedback':
        // Check if changes were made since rejection
        return document.lastModified > document.rejectedDate;

      default:
        return true;
    }
  }

  validateStateSpecificRules(fromState, toState, document, data) {
    const errors = [];

    // Draft → Submitted
    if (fromState === 'draft' && toState === 'submitted') {
      if (!document.amount || document.amount <= 0) {
        errors.push('Amount must be greater than zero');
      }

      if (!document.category) {
        errors.push('Category is required');
      }

      if (!document.description || document.description.length < 10) {
        errors.push('Description must be at least 10 characters');
      }

      if (!document.receipts || document.receipts.length === 0) {
        errors.push('At least one receipt is required');
      }
    }

    // Under Review → Approved
    if (fromState === 'under_review' && toState === 'approved') {
      if (!data.reviewer_notes) {
        errors.push('Reviewer notes are required for approval');
      }
    }

    // Under Review → Rejected
    if (fromState === 'under_review' && toState === 'rejected') {
      if (!data.rejection_reason) {
        errors.push('Rejection reason is required');
      }

      if (!data.changes_requested) {
        errors.push('Must specify what changes are needed');
      }
    }

    return {
      valid: errors.length === 0,
      errors: errors
    };
  }

  executeTransition(document, toState, actor, data) {
    // Validate first
    const validation = this.validateTransition(document, toState, actor, data);

    if (!validation.valid) {
      throw new Error(`Invalid transition: ${validation.errors.join(', ')}`);
    }

    // Record transition
    const transition = {
      from: document.state,
      to: toState,
      action: validation.transition.action,
      actor: actor,
      timestamp: new Date(),
      data: data
    };

    // Update document
    document.state = toState;
    document.stateHistory = document.stateHistory || [];
    document.stateHistory.push(transition);

    // State-specific updates
    this.updateDocumentForState(document, toState, actor, data);

    return {
      success: true,
      newState: toState,
      transition: transition
    };
  }

  updateDocumentForState(document, newState, actor, data) {
    switch(newState) {
      case 'submitted':
        document.submittedDate = new Date();
        document.submitter = actor;
        break;

      case 'under_review':
        document.reviewStartDate = new Date();
        document.reviewer = actor;
        break;

      case 'approved':
        document.approvalDate = new Date();
        document.approver = actor;
        document.approvalNotes = data.reviewer_notes;
        break;

      case 'rejected':
        document.rejectedDate = new Date();
        document.rejectedBy = actor;
        document.rejectionReason = data.rejection_reason;
        document.changesRequested = data.changes_requested;
        break;

      case 'paid':
        document.paidDate = new Date();
        document.paidBy = actor;
        break;
    }
  }
}

5. State History & Audit

Track all state changes:

class StateHistoryTracker {
  constructor() {
    this.history = [];
  }

  recordTransition(transition) {
    this.history.push({
      timestamp: transition.timestamp,
      from: transition.from,
      to: transition.to,
      action: transition.action,
      actor: transition.actor,
      data: transition.data
    });
  }

  getHistory(document) {
    return document.stateHistory || [];
  }

  renderTimeline(document) {
    const history = this.getHistory(document);

    let html = '<div class="state-timeline">';
    html += '<h3>Document History</h3>';
    html += '<ol class="timeline">';

    history.forEach(event => {
      html += `
        <li class="timeline-event state-${event.to}">
          <div class="event-time">${this.formatTime(event.timestamp)}</div>
          <div class="event-state">${event.to}</div>
          <div class="event-actor">${event.actor}</div>
          <div class="event-action">${event.action}</div>
        </li>
      `;
    });

    html += '</ol>';
    html += '</div>';

    return html;
  }

  formatTime(timestamp) {
    return new Date(timestamp).toLocaleString();
  }

  getStateMetrics(document) {
    const history = this.getHistory(document);

    return {
      totalTransitions: history.length,
      timeInDraft: this.calculateTimeInState(history, 'draft'),
      timeInReview: this.calculateTimeInState(history, 'under_review'),
      totalProcessingTime: this.calculateTotalTime(history),
      actors: this.getUniqueActors(history)
    };
  }

  calculateTimeInState(history, state) {
    let totalTime = 0;
    let enterTime = null;

    history.forEach(event => {
      if (event.to === state) {
        enterTime = event.timestamp;
      } else if (enterTime && event.from === state) {
        totalTime += event.timestamp - enterTime;
        enterTime = null;
      }
    });

    return totalTime;
  }

  calculateTotalTime(history) {
    if (history.length === 0) return 0;

    const first = history[0].timestamp;
    const last = history[history.length - 1].timestamp;

    return last - first;
  }

  getUniqueActors(history) {
    const actors = new Set();
    history.forEach(event => actors.add(event.actor));
    return Array.from(actors);
  }
}

6. Visual State Communication

Make state obvious to users:

class StateVisualization {
  renderStateBadge(state) {
    const stateInfo = this.getStateInfo(state);

    return `
      <div class="state-badge badge-${state}" 
           style="background-color: ${stateInfo.color}">
        <span class="badge-icon">${stateInfo.icon}</span>
        <span class="badge-text">${stateInfo.name}</span>
      </div>
    `;
  }

  renderWorkflowDiagram(currentState, stateMachine) {
    let html = '<div class="workflow-diagram">';

    // Get all states
    const states = Object.keys(stateMachine.states);

    states.forEach((state, index) => {
      const stateInfo = stateMachine.states[state];
      const isActive = state === currentState;

      html += `
        <div class="workflow-state ${isActive ? 'active' : ''}">
          <div class="state-circle" style="background-color: ${stateInfo.color}">
            ${stateInfo.icon}
          </div>
          <div class="state-label">${stateInfo.name}</div>
        </div>
      `;

      // Add arrow if not last
      if (index < states.length - 1) {
        html += '<div class="workflow-arrow">→</div>';
      }
    });

    html += '</div>';
    return html;
  }

  renderPermissionIndicator(field, canEdit, currentState) {
    if (canEdit) {
      return `
        <span class="permission-indicator editable" 
              title="You can edit this field in ${currentState} state">
          ✏️ Editable
        </span>
      `;
    } else {
      return `
        <span class="permission-indicator readonly" 
              title="This field is locked in ${currentState} state">
          🔒 Locked
        </span>
      `;
    }
  }

  renderStateHelp(currentState, availableTransitions) {
    return `
      <div class="state-help">
        <h4>Current State: ${currentState}</h4>
        <p>What you can do:</p>
        <ul>
          ${availableTransitions.map(t => `
            <li>${t.label}</li>
          `).join('')}
        </ul>
      </div>
    `;
  }
}

Implementation Details

Complete State-Aware System

class StateAwareFormSystem {
  constructor(documentType, config) {
    this.stateMachine = new DocumentStateMachine(documentType);
    this.stateConfig = new StateConfiguration(this.stateMachine);
    this.renderer = new StateAwareRenderer(this.stateMachine, this.stateConfig);
    this.validator = new StateTransitionValidator(this.stateMachine);
    this.history = new StateHistoryTracker();
    this.visualization = new StateVisualization();
  }

  render(document, currentUser) {
    return this.renderer.render(document, currentUser);
  }

  canPerformAction(document, action, actor) {
    const currentState = document.state;
    const transitions = this.stateMachine.getAvailableTransitions(
      currentState,
      actor,
      document
    );

    return transitions.some(t => t.action === action);
  }

  performAction(document, action, actor, data) {
    // Find the transition for this action
    const currentState = document.state;
    const transition = this.stateMachine.transitions.find(t =>
      t.from === currentState && t.action === action
    );

    if (!transition) {
      throw new Error(`No such action: ${action} in state ${currentState}`);
    }

    // Execute transition
    const result = this.validator.executeTransition(
      document,
      transition.to,
      actor,
      data
    );

    // Record in history
    this.history.recordTransition(result.transition);

    return result;
  }

  getFormConfiguration(document, currentUser) {
    const state = document.state;
    const config = this.stateConfig.getConfiguration(state);
    const transitions = this.stateMachine.getAvailableTransitions(
      state,
      currentUser,
      document
    );

    return {
      state: state,
      config: config,
      availableActions: transitions,
      stateInfo: this.stateMachine.states[state]
    };
  }
}

// Example usage
const expenseSystem = new StateAwareFormSystem('expense_report', {
  // Configuration
});

// Render form for user
const html = expenseSystem.render(expenseReport, currentUser);

// Attempt to submit
if (expenseSystem.canPerformAction(expenseReport, 'submit', currentUser)) {
  expenseSystem.performAction(expenseReport, 'submit', currentUser, {});
} else {
  console.log('Cannot submit in current state');
}

Consequences

Benefits

Workflow Integrity: - Only valid transitions allowed - Business process enforced - No invalid states

Clear Permissions: - Users know what they can do - Role-appropriate actions - No accidental edits

Better UX: - Form adapts to context - Appropriate actions shown - Clear state indication

Audit Trail: - All transitions tracked - Complete history - Who did what when

Reduced Errors: - Can't approve draft - Can't edit approved - State prevents mistakes

Liabilities

Complexity: - State machines are complex - Many states to manage - Testing all paths hard

Rigidity: - Strict workflow enforcement - May block edge cases - Override mechanisms needed

User Learning Curve: - Understanding states takes time - "Why can't I edit this?" - Need clear communication

Development Overhead: - More code than simple forms - State configuration detailed - Maintenance burden

Performance: - State checks add overhead - Permission evaluation expensive - Need optimization

Domain Examples

Healthcare: Patient Records

// Medical record states
defineStates() {
  return {
    'in_progress': { name: 'In Progress' },
    'ready_for_review': { name: 'Ready for Review' },
    'reviewed': { name: 'Reviewed by Physician' },
    'signed': { name: 'Signed & Locked' }
  };
}
// Contract states
defineStates() {
  return {
    'draft': { name: 'Draft' },
    'internal_review': { name: 'Internal Review' },
    'sent_to_client': { name: 'Sent to Client' },
    'client_review': { name: 'Under Client Review' },
    'negotiation': { name: 'In Negotiation' },
    'approved': { name: 'Approved' },
    'signed': { name: 'Signed' },
    'executed': { name: 'Executed' }
  };
}

Finance: Invoice Processing

// Invoice states
defineStates() {
  return {
    'created': { name: 'Created' },
    'submitted': { name: 'Submitted for Approval' },
    'approved': { name: 'Approved' },
    'scheduled': { name: 'Scheduled for Payment' },
    'paid': { name: 'Paid' },
    'disputed': { name: 'Disputed' },
    'cancelled': { name: 'Cancelled' }
  };
}

Prerequisites: - Volume 3, Pattern 13: Conditional Requirements (requirements change by state) - Volume 3, Pattern 14: Cross-Field Validation (validation strictness varies)

Synergies: - Volume 3, Pattern 16: Temporal Validation (time-based state transitions) - Volume 3, Pattern 18: Audit Trail (track state changes) - Volume 3, Pattern 19: Version Control (state affects versioning)

Conflicts: - Extreme flexibility needs - Ad-hoc workflows - Constantly changing processes

Alternatives: - Always editable (no states) - Separate forms per state (different URLs) - Manual workflow (humans track state)

Known Uses

Document Management: Draft → Review → Approved → Published

E-commerce Orders: Cart → Placed → Processing → Shipped → Delivered

HR Systems: Application → Interview → Offer → Hired

Project Management: Planned → In Progress → Review → Complete

CRM: Lead → Qualified → Proposal → Negotiation → Closed

Bug Tracking: New → Assigned → In Progress → Fixed → Verified → Closed

Approval Workflows: All enterprise systems with multi-step approval


Further Reading

Academic Foundations

  • Finite State Machines: Hopcroft, J.E., Motwani, R., & Ullman, J.D. (2006). Introduction to Automata Theory, Languages, and Computation (3rd ed.). Pearson. ISBN: 978-0321455363
  • Statecharts: Harel, D. (1987). "Statecharts: A Visual Formalism for Complex Systems." Science of Computer Programming 8(3): 231-274.
  • Workflow Patterns: van der Aalst, W.M.P., et al. (2003). "Workflow Patterns." Distributed and Parallel Databases 14(1): 5-51. http://www.workflowpatterns.com/

Practical Implementation

Standards & Specifications

  • Pattern 5: Form State Tracking - Basic state management
  • Pattern 11: Validation Rules - State-dependent validation
  • Pattern 23: Audit Trail - Log state transitions
  • Pattern 24: Version Control - Version at state changes
  • Volume 2, Pattern 9: Early Warning Signals - Predict next states
  • Volume 1, Chapter 8: Architecture of Domain-Specific Systems - Workflow architecture

Tools & Libraries

Implementation Examples