DAY 16 WEEK 4: INTEGRATION

CRUD Service & Workflows

Master the Orchestration Service for business workflows, external system integration, approval processes, and real-time workflow monitoring from plugins.

Identity and access management isn't just about authenticationβ€”it's about orchestrating business processes. When an employee needs access to a system, there's a whole workflow: validation, approval, provisioning, notification. The CRUD Service handles all of this, and your plugins can trigger and monitor these workflows.

πŸ“‹Learning Objectives

  • Understand the CRUD/Orchestration Service architecture
  • Read and write workflow definitions in YAML
  • Configure connectors for external systems
  • Trigger workflows via API and from plugins
  • Monitor workflow progress with SSE
  • Use CrudGrid component in plugins

πŸ—οΈOrchestration Service Architecture

The CRUD Service (Orchestration Service) is the workflow engine of EmpowerNow. It handles everything from simple data operations to complex multi-step approval processes.

flowchart TB subgraph CLIENT["Clients"] UI["Experience UI"] API["API Clients"] PLUGIN["Plugins"] end subgraph BFF["BFF Layer"] PROXY["API Proxy"] end subgraph CRUD["CRUD Service"] WE["Workflow Engine"] CM["Connector Manager"] TQ["Task Queue
(Redis)"] WD["Workflow Definitions"] WN["Worker Pool"] SSE["SSE Publisher"] end subgraph EXT["External Systems"] AD["Active Directory"] LDAP["LDAP"] REST["REST APIs"] DB["Databases"] SNOW["ServiceNow"] end UI --> PROXY API --> PROXY PLUGIN --> PROXY PROXY --> WE WE --> WD WE --> TQ WE --> SSE TQ --> WN WN --> CM CM --> AD CM --> LDAP CM --> REST CM --> DB CM --> SNOW style CRUD fill:#f9731622,stroke:#f97316 style EXT fill:#06b6d422,stroke:#06b6d4 style CLIENT fill:#a855f722,stroke:#a855f7

Key Components

βš™οΈ

Workflow Engine

Parses YAML definitions and orchestrates step execution

πŸ”Œ

Connector Manager

Routes actions to appropriate external system connectors

πŸ“‹

Task Queue

Redis-backed async processing for reliable execution

πŸ‘₯

Worker Pool

Scalable workers for parallel workflow processing

πŸ“‘

SSE Publisher

Real-time workflow status updates to clients

πŸ“„

Workflow Definitions

YAML-based declarative workflow specifications

πŸ“„Workflow Definition Structure

Workflows are defined in YAML with a clear structure: metadata, triggers, steps, and error handling.

Complete Workflow Example

# workflows/access-request.yaml
name: access-request
version: "1.0"
description: Request access to a protected resource
pdp_application: access-workflows  # Authorization scope

# How the workflow is started
trigger:
  type: manual
  form:
    title: "Access Request"
    fields:
      - name: resource_id
        type: string
        label: "Resource"
        required: true
        lookup: resources  # Dropdown from resources endpoint
      - name: justification
        type: textarea
        label: "Business Justification"
        required: true
        min_length: 20

# Input validation
validate:
  - condition: "{{ len(context.justification) >= 20 }}"
    error: "Justification must be at least 20 characters"

# Workflow steps
steps:
  - id: get-resource-details
    type: connector
    connector: membership
    action: get_resource
    input:
      resource_id: "{{ context.resource_id }}"
    output: resource

  - id: get-approvers
    type: connector
    connector: membership
    action: get_resource_approvers
    input:
      resource_id: "{{ context.resource_id }}"
    output: approvers

  - id: request-approval
    type: approval
    title: "Access Request: {{ steps.get-resource-details.output.resource.name }}"
    description: |
      User {{ context.requester_name }} is requesting access.
      Justification: {{ context.justification }}
    approvers: "{{ steps.get-approvers.output.approvers }}"
    min_approvals: 1
    timeout: 72h
    escalation:
      after: 48h
      to: "{{ steps.get-resource-details.output.resource.owner }}"

  - id: grant-access
    type: connector
    connector: membership
    action: assign_role
    condition: "{{ steps.request-approval.approved }}"
    input:
      user_id: "{{ context.requester_id }}"
      role_id: "{{ steps.get-resource-details.output.resource.default_role }}"
      location_id: "{{ steps.get-resource-details.output.resource.location_id }}"
      expires_at: "{{ now() + duration('30d') }}"
    output: assignment

  - id: notify-granted
    type: notification
    condition: "{{ steps.request-approval.approved }}"
    template: access-granted
    recipients:
      - "{{ context.requester_email }}"
    data:
      resource_name: "{{ steps.get-resource-details.output.resource.name }}"
      expires_at: "{{ steps.grant-access.output.assignment.expires_at }}"

  - id: notify-denied
    type: notification
    condition: "{{ not steps.request-approval.approved }}"
    template: access-denied
    recipients:
      - "{{ context.requester_email }}"
    data:
      resource_name: "{{ steps.get-resource-details.output.resource.name }}"
      denial_reason: "{{ steps.request-approval.denial_reason }}"

# Error handling
on_error:
  - id: notify-failure
    type: notification
    template: workflow-error
    recipients:
      - "{{ context.requester_email }}"
      - ops-team@company.com
    data:
      error: "{{ error.message }}"
      step: "{{ error.step_id }}"

πŸ”’Step Types

Type Description Key Properties
connector Call external system via connector connector, action, input, output
approval Human approval gate approvers, min_approvals, timeout, escalation
notification Send email/SMS/push template, recipients, data
script Execute Python/JS logic language, script
parallel Run multiple steps concurrently branches, wait_for
condition Branching logic if, then, else
loop Iterate over collection items, as, steps
wait Pause for duration or event duration or until
subworkflow Call another workflow workflow, input, output

Expression Syntax

Workflows use Jinja2-style expressions for dynamic values:

# Context variables
{{ context.requester_id }}
{{ context.form_data.justification }}

# Step outputs
{{ steps.get-approvers.output.approvers }}
{{ steps.request-approval.approved }}

# Built-in functions
{{ now() }}                          # Current timestamp
{{ now() + duration('7d') }}         # 7 days from now
{{ len(items) }}                     # List length
{{ items | join(', ') }}             # Join list
{{ user.email | lower }}             # Lowercase string

# Conditionals
{{ 'admin' if user.is_admin else 'user' }}
{{ items | selectattr('active', 'true') | list }}

πŸ”ŒConnectors

Connectors bridge workflows to external systems. Each connector type has specific actions it can perform.

Built-in Connector Types

🏒

Active Directory

User/group management, password reset, attribute sync

πŸ“

LDAP

Generic LDAP operations, search, modify

🌐

REST

HTTP calls to any REST API

πŸ—„οΈ

Database

SQL queries and stored procedures

πŸ“§

Email (SMTP)

Send templated emails

🎫

ServiceNow

Create tickets, update incidents

Connector Configuration

# connectors/servicenow.yaml
name: servicenow
type: rest
description: ServiceNow ITSM Integration

config:
  base_url: "https://company.service-now.com/api/now"
  auth:
    type: basic
    username: "{{ secrets.snow_username }}"
    password: "{{ secrets.snow_password }}"
  headers:
    Content-Type: application/json
    Accept: application/json

actions:
  create_incident:
    method: POST
    path: /table/incident
    body:
      short_description: "{{ input.title }}"
      description: "{{ input.description }}"
      caller_id: "{{ input.user_email }}"
      category: "{{ input.category }}"
      urgency: "{{ input.urgency | default(3) }}"
    response_mapping:
      incident_number: "{{ response.result.number }}"
      sys_id: "{{ response.result.sys_id }}"

  get_incident:
    method: GET
    path: "/table/incident/{{ input.sys_id }}"
    response_mapping:
      state: "{{ response.result.state }}"
      resolution_notes: "{{ response.result.close_notes }}"

  close_incident:
    method: PATCH
    path: "/table/incident/{{ input.sys_id }}"
    body:
      state: 7  # Closed
      close_code: "{{ input.close_code }}"
      close_notes: "{{ input.resolution }}"
🌐 NowConnect for On-Premises Systems

What if your Active Directory or LDAP is on-premises and can't be reached from the cloud? NowConnect creates secure, outbound-only tunnels from your datacenterβ€”no firewall holes, no VPN complexity. Learn more in Week 5, Day 24.

πŸ”—Plugin SDK Integration

Your plugins can trigger workflows, monitor their progress, and display workflow data using the sdk.crud namespace. This is how the Experience Platform becomes a unified interface for business processes.

Triggering Workflows from Plugins

// src/components/AccessRequestButton.tsx
const React = (globalThis as any).React;
const { useState } = React;
const sdk = (globalThis as any).pluginSdk;

export function AccessRequestButton({ resourceId }: { resourceId: string }) {
  const [loading, setLoading] = useState(false);
  const [runId, setRunId] = useState<string | null>(null);

  const requestAccess = async () => {
    setLoading(true);
    try {
      // Trigger the workflow
      const result = await sdk.crud.run('access-request', {
        resource_id: resourceId,
        justification: 'Need access for project work'
      });
      
      setRunId(result.workflow_run_id);
      console.log('Workflow started:', result);
    } catch (err) {
      console.error('Failed to start workflow:', err);
    } finally {
      setLoading(false);
    }
  };

  return React.createElement('div', null,
    React.createElement('button', {
      onClick: requestAccess,
      disabled: loading,
      style: buttonStyle
    }, loading ? 'Requesting...' : 'πŸ”‘ Request Access'),
    
    runId && React.createElement('p', null, 
      'Request submitted: ', runId
    )
  );
}

Monitoring Workflow Progress

// Subscribe to workflow status updates
export function WorkflowProgress({ runId }: { runId: string }) {
  const [status, setStatus] = useState<any>(null);

  useEffect(() => {
    // Subscribe to SSE for real-time updates
    const unsubscribe = sdk.sse.subscribe(
      `/api/crud/workflows/runs/${runId}/events`,
      (event) => {
        console.log('Workflow event:', event);
        setStatus(event);
      }
    );

    return () => unsubscribe();
  }, [runId]);

  if (!status) {
    return React.createElement('div', null, 'Loading...');
  }

  return React.createElement('div', { style: progressStyle },
    React.createElement('div', { style: statusBadgeStyle(status.status) },
      getStatusIcon(status.status), ' ', status.status
    ),
    React.createElement('p', null, 
      'Current step: ', status.current_step || 'N/A'
    ),
    status.status === 'waiting_approval' &&
      React.createElement('p', null, '⏳ Awaiting approval...')
  );
}

function getStatusIcon(status: string): string {
  const icons: Record<string, string> = {
    running: 'πŸ”„',
    waiting_approval: '⏳',
    completed: 'βœ…',
    failed: '❌',
    cancelled: '🚫'
  };
  return icons[status] || '❓';
}

Using CrudGrid for Workflow Data

// Display workflow runs in a data grid
const CrudGrid = (globalThis as any).CrudGrid;

export function MyWorkflowRuns() {
  return React.createElement(CrudGrid, {
    endpoint: '/api/crud/workflows/runs',
    filters: {
      requester_id: 'me',
      status: ['running', 'waiting_approval', 'completed']
    },
    columns: [
      { field: 'workflow_name', header: 'Workflow', sortable: true },
      { field: 'status', header: 'Status', render: StatusBadge },
      { field: 'started_at', header: 'Started', type: 'datetime' },
      { field: 'current_step', header: 'Current Step' }
    ],
    actions: [
      { 
        label: 'View Details', 
        icon: 'πŸ‘οΈ',
        onClick: (row) => window.location.href = `/workflows/${row.id}`
      },
      {
        label: 'Cancel',
        icon: '🚫',
        condition: (row) => row.status === 'running',
        onClick: (row) => cancelWorkflow(row.id)
      }
    ],
    pageSize: 10,
    refreshInterval: 30000  // Auto-refresh every 30s
  });
}
πŸ’‘ CrudGrid Capabilities

CrudGrid is a powerful data table component that handles pagination, sorting, filtering, and actions. It's perfect for displaying workflow runs, approval queues, audit logs, and any tabular data from the CRUD service.

πŸ“‘Workflow API Reference

List Workflows

# Get all available workflow definitions
curl -k https://bff.self.empowernow.ai/api/crud/workflows \
  -H "Authorization: Bearer $ACCESS_TOKEN"

# Response
{
  "workflows": [
    {
      "name": "access-request",
      "version": "1.0",
      "description": "Request access to a protected resource",
      "trigger": { "type": "manual" }
    },
    {
      "name": "user-offboarding",
      "version": "1.0",
      "description": "Remove user access on termination",
      "trigger": { "type": "event", "event": "user.terminated" }
    }
  ]
}

Trigger Workflow

# Start a new workflow run
curl -k -X POST https://bff.self.empowernow.ai/api/crud/workflows/access-request/run \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "resource_id": "resource-finance-reports",
    "justification": "Need access to prepare Q4 financial analysis"
  }'

# Response
{
  "workflow_run_id": "run-7f3a2b1c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
  "workflow_name": "access-request",
  "status": "running",
  "started_at": "2026-01-21T10:30:00Z",
  "current_step": "get-resource-details"
}

Get Workflow Run Status

# Check status of a specific run
curl -k https://bff.self.empowernow.ai/api/crud/workflows/runs/run-7f3a2b1c... \
  -H "Authorization: Bearer $ACCESS_TOKEN"

# Response
{
  "id": "run-7f3a2b1c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
  "workflow_name": "access-request",
  "status": "waiting_approval",
  "started_at": "2026-01-21T10:30:00Z",
  "current_step": "request-approval",
  "steps_completed": [
    {
      "id": "get-resource-details",
      "status": "completed",
      "started_at": "2026-01-21T10:30:00Z",
      "completed_at": "2026-01-21T10:30:01Z"
    },
    {
      "id": "get-approvers",
      "status": "completed",
      "started_at": "2026-01-21T10:30:01Z",
      "completed_at": "2026-01-21T10:30:02Z"
    }
  ],
  "pending_approval": {
    "step_id": "request-approval",
    "approvers": ["manager@company.com"],
    "waiting_since": "2026-01-21T10:30:02Z",
    "timeout_at": "2026-01-24T10:30:02Z"
  }
}

Approve/Deny a Step

# Approve
curl -k -X POST https://bff.self.empowernow.ai/api/crud/workflows/runs/run-7f3a2b1c.../approve \
  -H "Authorization: Bearer $APPROVER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "step_id": "request-approval",
    "decision": "approve",
    "comment": "Approved for Q4 project"
  }'

# Deny
curl -k -X POST https://bff.self.empowernow.ai/api/crud/workflows/runs/run-7f3a2b1c.../approve \
  -H "Authorization: Bearer $APPROVER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "step_id": "request-approval",
    "decision": "deny",
    "comment": "Access not needed for this role"
  }'

πŸ”§Troubleshooting

Issue: Workflow stuck in "running" state

# Check workflow run details for errors
curl -k https://bff.self.empowernow.ai/api/crud/workflows/runs/run-xxx \
  -H "Authorization: Bearer $TOKEN"

# Look for:
# - "error" field in step details
# - "retries" count (hitting retry limit?)
# - Connector configuration issues

# Common causes:
# - External system timeout
# - Invalid expression in step
# - Missing required input

Issue: Connector authentication failing

# Verify connector credentials
curl -k -X POST https://bff.self.empowernow.ai/api/crud/connectors/servicenow/test \
  -H "Authorization: Bearer $ADMIN_TOKEN"

# Check:
# - Secrets are properly configured
# - Network connectivity (NowConnect if on-prem)
# - Service account permissions in target system

Issue: Approval notifications not sent

# Verify notification template exists
curl -k https://bff.self.empowernow.ai/api/crud/templates/access-request-approval \
  -H "Authorization: Bearer $TOKEN"

# Check SMTP connector configuration
curl -k https://bff.self.empowernow.ai/api/crud/connectors/email/status \
  -H "Authorization: Bearer $ADMIN_TOKEN"

# Review approvers list in workflow run
# - Are email addresses valid?
# - Did the get-approvers step return results?

Issue: Expression evaluation error

# Check workflow logs for expression errors
curl -k https://bff.self.empowernow.ai/api/crud/workflows/runs/run-xxx/logs \
  -H "Authorization: Bearer $TOKEN"

# Common expression errors:
# - KeyError: variable doesn't exist in context
# - AttributeError: accessing property on None
# - TypeError: wrong type in operation

# Use default filter to handle missing values:
{{ context.optional_field | default('fallback') }}

❓Knowledge Check

1 What's the difference between workflow triggers "manual" and "event"? β–Ό

Manual: Workflow is started by explicit API call or form submission from a user.

Event: Workflow starts automatically when a system event occurs (e.g., user.terminated, role.assigned). Event-driven workflows enable automation.

2 How do you access data from a previous step in a workflow? β–Ό

Use the steps object with the step ID:

{{ steps.get-approvers.output.approvers }}
{{ steps.request-approval.approved }}

Each step's output object contains the data returned by that step.

3 What SDK method triggers a workflow from a plugin? β–Ό

sdk.crud.run(workflowName, inputData)

const result = await sdk.crud.run('access-request', {
  resource_id: 'resource-123',
  justification: 'Need for project'
});
console.log(result.workflow_run_id);
4 What happens when an approval step times out? β–Ό

If escalation is configured, the request is sent to the escalation target. If no escalation or escalation also times out, the step fails and the workflow's on_error handlers execute.

You can configure timeout_action: deny to auto-deny instead of failing.

5 How do plugins receive real-time workflow updates? β–Ό

Using Server-Sent Events (SSE) via the SDK:

sdk.sse.subscribe(
  `/api/crud/workflows/runs/${runId}/events`,
  (event) => {
    console.log('Status:', event.status);
  }
);

Events are pushed when workflow status changes, steps complete, or approvals are received.

πŸ“Day 16 Checkpoint

  • Understand Orchestration Service architecture
  • Can read and interpret workflow YAML definitions
  • Know the different step types and their properties
  • Understand connector configuration
  • Can trigger workflows via API and from plugins
  • Know how to monitor workflow progress with SSE
  • Can use CrudGrid to display workflow data
πŸŽ‰ What You Learned

The CRUD Service is the backbone of business process automation in EmpowerNow. You can now create workflows that orchestrate complex multi-step processes, integrate with external systems, and provide real-time status updates to users through your plugins.