CRUD Service & Workflows
Master the Orchestration Service for business workflows, external system integration, approval processes, and real-time workflow monitoring from plugins.
π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.
(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 }}"
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
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 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
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.
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.
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);
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.
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
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.