Automations
Learn how to build powerful event-driven automations in Stack9. This guide covers automation triggers, action chaining, conditional logic, error handling, and real-world implementation patterns from production systems.
What You'll Learn
- Automation structure and configuration
- All trigger types (entity lifecycle, webhooks, scheduled, queues)
- Event-driven automation patterns
- Conditional logic and action chaining
- Template expressions and dynamic parameters
- Error handling strategies
- Testing and debugging automations
- Real production examples
Time Required: 45-60 minutes
Prerequisites
- Completed Building Custom Actions guide
- Understanding of Action Types
- Basic knowledge of JSON and template expressions
- Familiarity with entity definitions
Understanding Stack9 Automations
Automations in Stack9 are configuration-driven workflows that respond to events and execute actions automatically. They consist of:
- Trigger - Event that starts the automation (afterCreate, webhook, cron, etc.)
- Actions - Sequence of operations to execute
- Conditional Actions - Actions that run only when conditions are met
- Parameters - Dynamic data passed using template expressions
Automations are defined as JSON files in src/automations/ and automatically registered when your Stack9 application starts.
Automation File Structure
Automations live in your project's automations directory:
src/
└── automations/
├── when_customer_created.json
├── after_wf_move_subscription.json
├── webhook_resend_receipt.json
├── sync_customer_data.json
└── trigger_send_deferral_reminder.json
Basic Automation Structure
Every automation follows this structure:
{
"key": "unique_automation_key",
"name": "Human-readable name",
"entityKey": "entity_name",
"app": "module_name",
"triggerType": "afterCreate",
"triggerParams": {},
"actions": [
{
"name": "Action description",
"key": "action_key",
"actionTypeKey": "action_type_to_execute",
"params": {
"param1": "{{trigger.entity.field}}",
"param2": "static_value"
}
}
],
"conditionalActions": []
}
Required Properties
| Property | Type | Description |
|---|---|---|
key | string | Unique identifier (snake_case) |
name | string | Display name for the automation |
entityKey | string | Entity this automation relates to (optional for some triggers) |
triggerType | string | Type of trigger (see Trigger Types) |
triggerParams | object | Configuration for the trigger |
actions | array | List of actions to execute |
conditionalActions | array | Actions with conditions (optional) |
Trigger Types
Stack9 supports several trigger types:
1. afterCreate - Entity Creation
Fires after a new entity record is created.
{
"key": "when_customer_created",
"name": "When Customer Created",
"entityKey": "customer",
"app": "crm",
"triggerType": "afterCreate",
"triggerParams": {},
"actions": [
{
"name": "Send welcome email",
"key": "send_welcome",
"actionTypeKey": "send_welcome_email",
"params": {
"customerId": "{{trigger.entity.id}}",
"email": "{{trigger.entity.email}}",
"firstName": "{{trigger.entity.first_name}}"
}
}
]
}
Available trigger data:
trigger.entity- The created entity recordtrigger.entityId- The ID of the created entitytrigger.userId- User who created the record
2. afterUpdate - Entity Update
Fires after an entity record is updated.
{
"key": "when_subscription_updated",
"name": "When Subscription Updated",
"entityKey": "subscription",
"triggerType": "afterUpdate",
"triggerParams": {},
"actions": [
{
"name": "Add to queue subscription",
"key": "add_to_queue_subscription",
"actionTypeKey": "add_message_queue",
"params": {
"queueName": "index_subscription",
"message": "{{{subscriptionId: trigger.entity.id}}}",
"priority": "long"
}
},
{
"name": "After subscription updated",
"key": "after_subscription_updated",
"actionTypeKey": "after_subscription_updated",
"params": {
"entityId": "{{trigger.entity.id}}"
}
},
{
"name": "Check Subscription Condition",
"key": "check_subscription_condition",
"actionTypeKey": "check_subscription_workflow_conditions",
"params": {
"subscriptionId": "{{trigger.entity.id}}"
}
},
{
"name": "Update Subscription WF Condition",
"key": "update_subscription_wf_condition",
"actionTypeKey": "workflow_step_condition_upsert",
"params": {
"entityKey": "subscription",
"entityId": "{{runbook.outputs.check_subscription_condition.subscriptionId}}",
"conditions": "{{runbook.outputs.check_subscription_condition.conditions}}"
}
}
]
}
Available trigger data:
trigger.entity- The updated entity (new values)trigger.oldEntity- The entity before update (original values)trigger.entityId- The ID of the updated entitytrigger.userId- User who updated the record
Use case: Detect field changes, sync to external systems, update related records
3. afterDelete - Entity Deletion
Fires after an entity record is deleted.
{
"key": "when_customer_deleted",
"name": "When Customer Deleted",
"entityKey": "customer",
"triggerType": "afterDelete",
"triggerParams": {},
"actions": [
{
"name": "Clean up related data",
"key": "cleanup",
"actionTypeKey": "cleanup_customer_data",
"params": {
"customerId": "{{trigger.entityId}}"
}
}
]
}
4. afterWorkflowMove - Workflow State Change
Fires after an entity moves to a different workflow step.
{
"key": "after_wf_move_subscription",
"name": "After WF Move Subscription",
"entityKey": "subscription",
"triggerType": "afterWorkflowMove",
"triggerParams": {},
"actions": [
{
"name": "Update customer flags",
"key": "update_customer_flags",
"actionTypeKey": "update_customer_flags_from_subscription",
"params": {
"subscriptionId": "{{trigger.entityId}}"
}
},
{
"name": "Get subscription",
"key": "get_subscription",
"actionTypeKey": "entity_find",
"params": {
"entityKey": "subscription",
"query": "{{{$select: ['id', 'customer_id', '_workflow_outcome'], $where: {id: trigger.entityId}}}}"
}
}
],
"conditionalActions": [
{
"condition": {
"rules": [
{
"field": "{{trigger.actionKey}}",
"value": "reprocess_validation",
"operator": "equals"
}
],
"combinator": "or"
},
"actions": [
{
"name": "Execute validation step",
"key": "handle_step",
"actionTypeKey": "handle_subscription_validation_wf_step",
"params": {
"subscriptionId": "{{trigger.entityId}}"
}
}
]
},
{
"condition": {
"rules": [
{
"field": "{{trigger.actionKey}}",
"value": "pending_customer_match",
"operator": "equals"
},
{
"field": "{{trigger.actionKey}}",
"value": "reprocess_matching",
"operator": "equals"
}
],
"combinator": "or"
},
"actions": [
{
"name": "Execute customer match step",
"key": "handle_step",
"actionTypeKey": "handle_subscription_customer_match_wf_step",
"params": {
"subscriptionId": "{{trigger.entityId}}"
}
}
]
}
]
}
Available trigger data:
trigger.entityId- Entity IDtrigger.entity- Current entity statetrigger.actionKey- Workflow action that was takentrigger.workflowData- Workflow transition details
Use case: Execute logic based on workflow steps, implement multi-stage processes
5. webhook - HTTP Endpoint
Creates an HTTP endpoint that triggers the automation.
{
"key": "webhook_resend_receipt",
"name": "Resend Receipt",
"entityKey": "sales_order",
"triggerType": "webhook",
"triggerParams": {
"method": "post",
"path": "/resend-receipt"
},
"actions": [
{
"name": "Resend receipt",
"key": "resend_receipt",
"actionTypeKey": "resend_receipt",
"params": {
"outbound": "{{trigger.body}}"
}
}
]
}
triggerParams for webhook:
method- HTTP method: "post", "get", "put", "all"path- URL path (e.g., "/resend-receipt")
Available trigger data:
trigger.body- Request body (JSON)trigger.query- Query parameterstrigger.params- URL parameterstrigger.headers- HTTP headers
Webhook URL: https://your-app.com/webhooks/{path}
Use case: External integrations, API endpoints, third-party webhooks
6. cronJob - Scheduled Execution
Runs on a schedule (requires registration in app.json).
{
"key": "scheduled_cleanup",
"name": "Daily Cleanup Job",
"triggerType": "cronJob",
"triggerParams": {
"cronExpression": "0 2 * * *",
"timeoutMs": 300000
},
"actions": [
{
"name": "Clean old records",
"key": "cleanup",
"actionTypeKey": "cleanup_old_records",
"params": {}
}
]
}
triggerParams for cronJob:
cronExpression- Cron schedule expressiontimeoutMs- Maximum execution time in milliseconds
Cron schedule examples:
0 * * * *- Every hour0 9 * * *- Every day at 9 AM0 9 * * 1- Every Monday at 9 AM*/15 * * * *- Every 15 minutes0 0 1 * *- First day of every month
Use case: Batch processing, cleanup jobs, reports, synchronization
7. mqHandler - Message Queue Consumer
Processes messages from a queue.
{
"key": "sync_customer_data",
"name": "Sync Customer Data",
"entityKey": "customer",
"triggerType": "mqHandler",
"triggerParams": {
"queueName": "index_customer",
"timeoutMs": 5000
},
"actions": [
{
"name": "Sync customer data",
"key": "sync_customer_data",
"actionTypeKey": "sync_customer_data",
"params": {
"customerId": "{{trigger.message.body.customerId}}"
}
}
]
}
triggerParams for mqHandler:
queueName- Name of the queue to consume fromtimeoutMs- Processing timeout in milliseconds
Available trigger data:
trigger.message.body- Message payloadtrigger.automationKey- Automation key
Use case: Asynchronous processing, background jobs, scalable processing
Actions
Actions are the operations executed when an automation triggers. They run sequentially in the order defined.
Action Structure
{
"name": "Human-readable description",
"key": "unique_action_key",
"actionTypeKey": "action_type_to_execute",
"params": {
"param1": "{{dynamic_value}}",
"param2": "static_value"
}
}
Accessing Previous Action Results
Actions can reference outputs from previous actions:
{
"actions": [
{
"name": "Fetch customer",
"key": "fetch_customer",
"actionTypeKey": "entity_find",
"params": {
"entityKey": "customer",
"query": "{{{$where: {id: trigger.entity.customer_id}}}}"
}
},
{
"name": "Send email to customer",
"key": "send_email",
"actionTypeKey": "send_email",
"params": {
"to": "{{runbook.outputs.fetch_customer.email}}",
"subject": "Hello {{runbook.outputs.fetch_customer.first_name}}!"
}
}
]
}
Pattern: Use runbook.outputs.{action_key}.{field} to access action results.
Conditional Actions
Conditional actions only execute when specific conditions are met.
Simple Condition
{
"conditionalActions": [
{
"condition": {
"rules": [
{
"field": "{{trigger.entity.status}}",
"value": "active",
"operator": "equals"
}
],
"combinator": "and"
},
"actions": [
{
"name": "Send activation email",
"key": "send_activation",
"actionTypeKey": "send_email",
"params": {
"to": "{{trigger.entity.email}}"
}
}
]
}
]
}
Multiple Conditions (OR Logic)
{
"condition": {
"rules": [
{
"field": "{{trigger.entity.status}}",
"value": "pending",
"operator": "equals"
},
{
"field": "{{trigger.entity.status}}",
"value": "review",
"operator": "equals"
}
],
"combinator": "or"
},
"actions": [...]
}
Multiple Conditions (AND Logic)
{
"condition": {
"rules": [
{
"field": "{{trigger.entity.status}}",
"value": "active",
"operator": "equals"
},
{
"field": "{{trigger.entity.subscription_tier}}",
"value": "premium",
"operator": "equals"
}
],
"combinator": "and"
},
"actions": [...]
}
Supported Operators
equals- Exact matchnotEquals- Not equalcontains- String contains substringstartsWith- String starts withendsWith- String ends withgreaterThan- Numeric greater thanlessThan- Numeric less thanin- Value in arraychanged- Field changed (afterUpdate only)
Template Expressions
Use double curly braces {{expression}} for dynamic values.
Trigger Context
{
"params": {
"entityId": "{{trigger.entity.id}}",
"email": "{{trigger.entity.email}}",
"oldStatus": "{{trigger.oldEntity.status}}",
"newStatus": "{{trigger.entity.status}}",
"userId": "{{trigger.userId}}",
"actionKey": "{{trigger.actionKey}}",
"webhookBody": "{{trigger.body.data}}",
"queryParam": "{{trigger.query.param_name}}"
}
}
Action Results
{
"params": {
"previousResult": "{{runbook.outputs.action_key.field_name}}",
"customerId": "{{runbook.outputs.fetch_customer.id}}",
"customerEmail": "{{runbook.outputs.fetch_customer.email}}"
}
}
Object Expressions
Use triple braces {{{object}}} for objects:
{
"params": {
"data": "{{{customer_id: trigger.entity.id, status: trigger.entity.status}}}"
}
}
Real Production Examples
Example 1: Customer Update Workflow
Full automation that handles customer updates with indexing and history tracking:
File: src/automations/when_customer_updated.json
{
"key": "when_customer_updated",
"name": "When Customer Updated",
"entityKey": "customer",
"triggerType": "afterUpdate",
"triggerParams": {},
"actions": [
{
"name": "Add to search index queue",
"key": "add_to_index",
"actionTypeKey": "add_message_queue",
"params": {
"queueName": "index_customer",
"message": "{{{customerId: trigger.entity.id}}}",
"priority": "high"
}
},
{
"name": "Check if email changed",
"key": "email_changed",
"actionTypeKey": "check_field_changed",
"params": {
"entity": "{{trigger.entity}}",
"oldEntity": "{{trigger.oldEntity}}",
"fieldName": "email"
}
},
{
"name": "Track history event",
"key": "track_history",
"actionTypeKey": "track_customer_event",
"params": {
"customerId": "{{trigger.entity.id}}",
"eventType": "customer_updated",
"changes": "{{{old: trigger.oldEntity, new: trigger.entity}}}"
}
}
],
"conditionalActions": [
{
"condition": {
"rules": [
{
"field": "{{runbook.outputs.email_changed.changed}}",
"value": "true",
"operator": "equals"
}
],
"combinator": "and"
},
"actions": [
{
"name": "Send email verification",
"key": "send_verification",
"actionTypeKey": "send_email_verification",
"params": {
"customerId": "{{trigger.entity.id}}",
"newEmail": "{{trigger.entity.email}}"
}
}
]
}
]
}
Example 2: Webhook Processing with Queue
Webhook that triggers async processing:
File: src/automations/trigger_send_deferral_reminder.json
{
"key": "trigger_send_deferral_reminder",
"name": "Trigger Send Deferral Reminder",
"entityKey": "subscription",
"triggerType": "webhook",
"triggerParams": {
"method": "post",
"path": "/trigger-send-deferral-reminder"
},
"actions": [
{
"name": "Add to queue",
"key": "add_to_queue",
"actionTypeKey": "add_message_queue",
"params": {
"queueName": "execute_send_deferral_reminder",
"message": "{{{}}}",
"priority": "long"
}
}
]
}
Example 3: Complex Workflow Move Handler
Handles workflow transitions with multiple conditional paths:
File: src/automations/after_wf_move_sales_order.json
{
"key": "after_wf_move_sales_order",
"name": "After WF Move Sales Order",
"entityKey": "sales_order",
"triggerType": "afterWorkflowMove",
"triggerParams": {},
"actions": [
{
"name": "Get sales order",
"key": "get_order",
"actionTypeKey": "entity_find",
"params": {
"entityKey": "sales_order",
"query": "{{{$where: {id: trigger.entityId}}}}"
}
},
{
"name": "Update index",
"key": "update_index",
"actionTypeKey": "add_message_queue",
"params": {
"queueName": "index_sales_order",
"message": "{{{salesOrderId: trigger.entityId}}}"
}
}
],
"conditionalActions": [
{
"condition": {
"rules": [
{
"field": "{{trigger.actionKey}}",
"value": "sendForPayment",
"operator": "equals"
}
],
"combinator": "and"
},
"actions": [
{
"name": "Process payment",
"key": "process_payment",
"actionTypeKey": "handle_sales_order_payment_wf_step",
"params": {
"salesOrderId": "{{trigger.entityId}}"
}
}
]
},
{
"condition": {
"rules": [
{
"field": "{{trigger.actionKey}}",
"value": "sendForReceipt",
"operator": "equals"
},
{
"field": "{{trigger.actionKey}}",
"value": "sendForPendingReceipt",
"operator": "equals"
}
],
"combinator": "or"
},
"actions": [
{
"name": "Generate receipt",
"key": "generate_receipt",
"actionTypeKey": "handle_sales_order_receipt_wf_step",
"params": {
"salesOrderId": "{{trigger.entityId}}"
}
}
]
},
{
"condition": {
"rules": [
{
"field": "{{trigger.actionKey}}",
"value": "success",
"operator": "equals"
}
],
"combinator": "and"
},
"actions": [
{
"name": "Handle success outcome",
"key": "handle_success",
"actionTypeKey": "handle_sales_order_success_wf_outcome",
"params": {
"salesOrderId": "{{trigger.entityId}}"
}
}
]
}
]
}
Example 4: Queue Handler with Timeout
Message queue processor with timeout:
File: src/automations/execute_check_bank_run.json
{
"key": "execute_check_bank_run",
"name": "Execute Check Bank Run",
"entityKey": "bank_payment_file",
"app": "fin",
"triggerType": "mqHandler",
"triggerParams": {
"queueName": "execute_check_bank_run",
"timeoutMs": 30000
},
"actions": [
{
"name": "Execute check bank run",
"key": "execute_check_bank_run",
"actionTypeKey": "execute_check_bank_run",
"params": {}
}
]
}
Example 5: Workflow Move with Queue
Trigger workflow transitions via queue:
File: src/automations/move_wf_subscription.json
{
"key": "move_wf_subscription",
"name": "Move WF Subscription",
"entityKey": "subscription",
"app": "crm",
"triggerType": "mqHandler",
"triggerParams": {
"queueName": "move_wf_subscription",
"timeoutMs": 30000
},
"actions": [
{
"name": "Handle workflow action",
"key": "handle_action",
"actionTypeKey": "workflow_move",
"params": {
"entityKey": "subscription",
"entityId": "{{trigger.message.body.entityId}}",
"actionKey": "{{trigger.message.body.actionKey}}",
"body": "{{{}}}"
}
}
]
}
Error Handling
Strategy 1: Graceful Degradation
Continue automation even if non-critical actions fail:
// In your action type
export class SyncToCRM extends S9AutomationActionType {
async exec(params: any) {
try {
// Attempt sync
await externalApi.sync(params);
return { success: true };
} catch (error) {
// Log error but don't fail automation
this.context.logger.error(`CRM sync failed: ${error.message}`);
return {
success: false,
skipped: true,
error: error.message
};
}
}
}
Strategy 2: Retry Logic
Implement retry for transient failures:
export class RetryableAction extends S9AutomationActionType {
async exec(params: any) {
const maxRetries = 3;
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
return await this.executeOperation(params);
} catch (error) {
lastError = error;
this.context.logger.warn(`Attempt ${i + 1} failed: ${error.message}`);
await this.delay(1000 * Math.pow(2, i)); // Exponential backoff
}
}
throw lastError;
}
}
Strategy 3: Dead Letter Queue
Move failed messages to error queue:
{
"actions": [
{
"name": "Process message",
"key": "process",
"actionTypeKey": "process_with_error_handling",
"params": {
"data": "{{trigger.message.body}}",
"errorQueue": "failed_messages"
}
}
]
}
Testing Automations
Test Entity Lifecycle Triggers
# Test afterCreate
curl -X POST http://localhost:3000/api/customer \
-H "Content-Type: application/json" \
-d '{
"first_name": "Test",
"last_name": "User",
"email": "test@example.com"
}'
# Test afterUpdate
curl -X PUT http://localhost:3000/api/customer/1 \
-H "Content-Type: application/json" \
-d '{
"status": "active"
}'
Test Webhooks
curl -X POST http://localhost:3000/webhooks/resend-receipt \
-H "Content-Type: application/json" \
-d '{
"salesOrderId": 123,
"email": "customer@example.com"
}'
Test Queue Messages
// Programmatically add message to queue
await messageQueue.add('index_customer', {
customerId: 123
});
Monitor Logs
npm run dev
# Watch for automation execution logs:
# [INFO] Automation 'when_customer_created' triggered
# [INFO] Action 'send_welcome' completed successfully
Best Practices
1. Use Descriptive Names
// Good
{
"key": "when_customer_verified",
"name": "When Customer Email Verified",
"actions": [
{
"name": "Send welcome email to verified customer",
"key": "send_welcome_email"
}
]
}
// Bad
{
"key": "auto1",
"name": "automation",
"actions": [
{
"name": "action",
"key": "a1"
}
]
}
2. Keep Actions Atomic
Each action should do one thing:
// Good - separate concerns
{
"actions": [
{
"name": "Validate order",
"key": "validate",
"actionTypeKey": "validate_order"
},
{
"name": "Process payment",
"key": "payment",
"actionTypeKey": "process_payment"
},
{
"name": "Send confirmation",
"key": "confirm",
"actionTypeKey": "send_confirmation"
}
]
}
// Bad - action does too much
{
"actions": [
{
"name": "Process everything",
"key": "process_all",
"actionTypeKey": "process_order_payment_and_send_email"
}
]
}
3. Use Queues for Heavy Operations
{
"triggerType": "afterCreate",
"actions": [
{
"name": "Queue heavy processing",
"key": "queue_processing",
"actionTypeKey": "add_message_queue",
"params": {
"queueName": "heavy_processing",
"message": "{{{entityId: trigger.entity.id}}}"
}
}
]
}
4. Log Extensively
export class MyAction extends S9AutomationActionType {
async exec(params: any) {
const { logger } = this.context;
logger.info(`Starting process for ID: ${params.id}`);
try {
const result = await this.process(params);
logger.info(`Process completed: ${result.status}`);
return result;
} catch (error) {
logger.error(`Process failed: ${error.message}`, { params, error });
throw error;
}
}
}
5. Validate Trigger Data
export class SafeAction extends S9AutomationActionType {
async exec(params: any) {
// Validate required parameters
if (!params.customerId) {
throw new Error('customerId is required');
}
// Check entity exists
const customer = await this.context.db('customer')
.where({ id: params.customerId })
.first();
if (!customer) {
throw new Error(`Customer ${params.customerId} not found`);
}
// Process...
}
}
Troubleshooting
Automation Not Triggering
Problem: Automation doesn't run when expected
Solutions:
- Check automation file is in
src/automations/ - Verify
triggerTypematches event - Check
entityKeymatches entity - Restart Stack9:
npm run dev - Look for errors in logs
Template Expression Not Working
Problem: {{trigger.entity.field}} not replaced
Solutions:
- Verify field name is correct
- Check entity has that field
- Use correct trigger context
- Test with simpler expression first
Action Not Executing
Problem: Action in sequence doesn't run
Solutions:
- Check
actionTypeKeyis registered - Verify previous actions succeeded
- Check conditional logic
- Review action logs for errors
Conditional Actions Not Running
Problem: Conditions not evaluating correctly
Solutions:
- Verify field values match exactly
- Check operator is correct
- Use
combinator: "or"vs"and"correctly - Log values for debugging
Performance Considerations
1. Avoid N+1 Queries
// Bad - N queries
for (const item of items) {
const related = await db('related').where({ id: item.related_id });
}
// Good - 1 query
const relatedIds = items.map(i => i.related_id);
const related = await db('related').whereIn('id', relatedIds);
2. Use Batch Processing
const BATCH_SIZE = 100;
const records = await db('entity').select();
for (let i = 0; i < records.length; i += BATCH_SIZE) {
const batch = records.slice(i, i + BATCH_SIZE);
await this.processBatch(batch);
}
3. Set Appropriate Timeouts
{
"triggerParams": {
"timeoutMs": 30000 // 30 seconds for long operations
}
}
Next Steps
Congratulations! You've mastered Stack9 automations. Continue learning:
- Workflows - Define multi-step approval workflows
- Integrating External APIs - Connect to third-party services
- Building Custom Actions - Create reusable action types
- Performance Optimization - Scale your automations
Summary
You now understand:
- Automation structure and configuration
- All trigger types (afterCreate, afterUpdate, webhook, cronJob, mqHandler, etc.)
- Action sequencing and chaining
- Conditional actions and logic
- Template expressions for dynamic parameters
- Error handling strategies
- Testing and debugging approaches
- Real production patterns
Automations are the backbone of event-driven applications in Stack9. Master them to build reactive, scalable systems!