Workflows
Build powerful automations with DAG-based workflows
Workflows let you automate business processes with a visual, DAG-based execution model. Unlike simple sequential automation tools, Arky workflows can run nodes in parallel, branch conditionally, and trigger from webhooks or schedules.
When to Use Workflows
| Use Case | Example |
|---|---|
| Event reactions | Send Slack notification when order placed |
| Data sync | Sync new customers to your CRM |
| Scheduled tasks | Generate daily reports at 9am |
| Multi-step processes | Order fulfillment with inventory check, shipping label, notification |
| Conditional logic | Different handling for high-value vs standard orders |
Your First Workflow
Letβs build a workflow that sends a Slack notification when a new order is placed.
Step 1: Create the Workflow
const workflow = await sdk.workflow.createWorkflow({
key: 'order-slack-notification',
status: 'active',
nodes: {
// Entry point - triggers when called
trigger: {
type: 'trigger'
},
// Send to Slack
notifySlack: {
type: 'http',
method: 'POST',
url: 'https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK',
headers: {
'Content-Type': 'application/json'
},
body: {
text: 'π New order from ${trigger.data.customerEmail}',
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: '*New Order*\nβ’ Customer: ${trigger.data.customerEmail}\nβ’ Total: $${trigger.data.total / 100}\nβ’ Items: ${trigger.data.itemCount}'
}
}
]
}
}
},
edges: [
{ id: 'e1', source: 'trigger', sourceOutput: 'default', target: 'notifySlack' }
]
});
console.log('Workflow created with secret:', workflow.val.webhookSecret);
Step 2: Configure Webhook in Your App
When an order is placed, trigger the workflow:
// In your order handler
async function handleNewOrder(order) {
// Save order to database
await saveOrder(order);
// Trigger the workflow
await sdk.workflow.triggerWorkflow({
secret: 'wh_your_workflow_secret',
orderId: order.id,
customerEmail: order.customerInfo.email,
total: order.total,
itemCount: order.items.length
});
}
Or trigger from Arky webhooks by configuring the workflow trigger:
trigger: {
type: 'trigger',
event: 'order.created' // Listens for this business event
}
Workflow Patterns
Pattern 1: Sequential Processing
Run steps one after another.
βββββββββββ βββββββββββ βββββββββββ
β Trigger ββββββΆβ Step 1 ββββββΆβ Step 2 β
βββββββββββ βββββββββββ βββββββββββ
{
nodes: {
trigger: { type: 'trigger' },
step1: { type: 'http', method: 'POST', url: '...' },
step2: { type: 'http', method: 'POST', url: '...' }
},
edges: [
{ id: 'e1', source: 'trigger', sourceOutput: 'default', target: 'step1' },
{ id: 'e2', source: 'step1', sourceOutput: 'default', target: 'step2' }
]
}
Pattern 2: Parallel Execution
Run multiple steps at the same time.
βββββββββββ
βββββΆβ Task A β
ββββββββββββ βββββββββββ
β Trigger ββ€
ββββββββββββ βββββββββββ
βββββΆβ Task B β
βββββββββββ
{
nodes: {
trigger: { type: 'trigger' },
taskA: { type: 'http', method: 'POST', url: 'https://api.example.com/a' },
taskB: { type: 'http', method: 'POST', url: 'https://api.example.com/b' }
},
edges: [
{ id: 'e1', source: 'trigger', sourceOutput: 'default', target: 'taskA' },
{ id: 'e2', source: 'trigger', sourceOutput: 'default', target: 'taskB' }
]
}
Parallel execution dramatically speeds up workflows. If two operations donβt depend on each other, connect them both directly to the previous node.
Pattern 3: Conditional Branching
Take different paths based on conditions.
βββββββββββββββ
βββββΆβ High Value β
βββββββββββ β βββββββββββββββ
β Check ββββββ€
β Amount β β βββββββββββββββ
βββββββββββ βββββΆβ Standard β
βββββββββββββββ
{
nodes: {
trigger: { type: 'trigger' },
checkAmount: {
type: 'if',
condition: 'trigger.data.total > 10000' // Over $100
},
highValueFlow: {
type: 'http',
method: 'POST',
url: 'https://hooks.slack.com/sales-team',
body: { text: 'π High-value order: $${trigger.data.total / 100}' }
},
standardFlow: {
type: 'http',
method: 'POST',
url: 'https://api.example.com/process-standard'
}
},
edges: [
{ id: 'e1', source: 'trigger', sourceOutput: 'default', target: 'checkAmount' },
{ id: 'e2', source: 'checkAmount', sourceOutput: 'true', target: 'highValueFlow' },
{ id: 'e3', source: 'checkAmount', sourceOutput: 'false', target: 'standardFlow' }
]
}
Pattern 4: Wait and Retry
Add delays between steps.
{
nodes: {
trigger: { type: 'trigger' },
sendEmail: {
type: 'http',
method: 'POST',
url: 'https://api.email.com/send',
body: { /* ... */ }
},
waitTwoDays: {
type: 'wait',
duration: '2d' // Supports: 30s, 5m, 2h, 1d
},
sendFollowUp: {
type: 'http',
method: 'POST',
url: 'https://api.email.com/send',
body: { /* follow-up email */ }
}
},
edges: [
{ id: 'e1', source: 'trigger', sourceOutput: 'default', target: 'sendEmail' },
{ id: 'e2', source: 'sendEmail', sourceOutput: 'default', target: 'waitTwoDays' },
{ id: 'e3', source: 'waitTwoDays', sourceOutput: 'default', target: 'sendFollowUp' }
]
}
Scheduled Workflows
Run workflows on a schedule using cron expressions.
const dailyReport = await sdk.workflow.createWorkflow({
key: 'daily-sales-report',
status: 'active',
schedule: '0 9 * * *', // Every day at 9:00 AM UTC
nodes: {
trigger: { type: 'trigger' },
fetchSales: {
type: 'http',
method: 'GET',
url: 'https://api.yourapp.com/sales/yesterday'
},
sendReport: {
type: 'http',
method: 'POST',
url: 'https://hooks.slack.com/services/xxx',
body: {
text: 'π Daily Sales Report\nTotal: $${fetchSales.response.total}\nOrders: ${fetchSales.response.orderCount}'
}
}
},
edges: [
{ id: 'e1', source: 'trigger', sourceOutput: 'default', target: 'fetchSales' },
{ id: 'e2', source: 'fetchSales', sourceOutput: 'default', target: 'sendReport' }
]
});
Cron Expression Reference
| Expression | Schedule |
|---|---|
0 9 * * * | Every day at 9:00 AM |
0 */2 * * * | Every 2 hours |
0 9 * * 1 | Every Monday at 9:00 AM |
0 0 1 * * | First day of every month |
*/15 * * * * | Every 15 minutes |
0 9 * * 1-5 | Weekdays at 9:00 AM |
Using Variables
Access data from trigger input and previous nodes using ${...} syntax.
Available Variables
| Variable | Description |
|---|---|
${trigger.data.*} | Data passed to the trigger |
${env.*} | Business environment variables |
${nodeId.response.*} | HTTP response from a previous node |
Example: Chaining API Calls
{
nodes: {
trigger: { type: 'trigger' },
// First: Get customer details
getCustomer: {
type: 'http',
method: 'GET',
url: 'https://api.crm.com/customers/${trigger.data.customerId}',
headers: {
'Authorization': 'Bearer ${env.CRM_API_KEY}'
}
},
// Second: Use customer data to create ticket
createTicket: {
type: 'http',
method: 'POST',
url: 'https://api.support.com/tickets',
headers: {
'Authorization': 'Bearer ${env.SUPPORT_API_KEY}'
},
body: {
customerName: '${getCustomer.response.name}',
customerEmail: '${getCustomer.response.email}',
issue: '${trigger.data.issueDescription}'
}
}
},
edges: [
{ id: 'e1', source: 'trigger', sourceOutput: 'default', target: 'getCustomer' },
{ id: 'e2', source: 'getCustomer', sourceOutput: 'default', target: 'createTicket' }
]
}
Complete Example: Order Fulfillment
A real-world workflow that handles order processing:
const fulfillmentWorkflow = await sdk.workflow.createWorkflow({
key: 'order-fulfillment',
status: 'active',
nodes: {
trigger: {
type: 'trigger',
event: 'order.payment_received'
},
// Check if high-value order
checkValue: {
type: 'if',
condition: 'trigger.data.total > 50000' // Over $500
},
// High value: Manual review required
flagForReview: {
type: 'http',
method: 'POST',
url: 'https://api.yourapp.com/orders/${trigger.data.orderId}/flag',
body: { reason: 'high_value', requiresApproval: true }
},
// Notify sales team about high-value order
notifySales: {
type: 'http',
method: 'POST',
url: 'https://hooks.slack.com/services/sales',
body: {
text: 'π° High-value order needs review: $${trigger.data.total / 100}'
}
},
// Standard flow: Check inventory
checkInventory: {
type: 'http',
method: 'POST',
url: 'https://api.inventory.com/check',
body: { items: '${trigger.data.items}' }
},
// Reserve inventory
reserveInventory: {
type: 'http',
method: 'POST',
url: 'https://api.inventory.com/reserve',
body: {
orderId: '${trigger.data.orderId}',
items: '${trigger.data.items}'
}
},
// Generate shipping label
createShippingLabel: {
type: 'http',
method: 'POST',
url: 'https://api.shipping.com/labels',
body: {
orderId: '${trigger.data.orderId}',
address: '${trigger.data.shippingAddress}',
weight: '${checkInventory.response.totalWeight}'
}
},
// Send confirmation email
sendConfirmation: {
type: 'http',
method: 'POST',
url: 'https://api.email.com/send',
body: {
to: '${trigger.data.customerEmail}',
template: 'order_shipped',
data: {
orderId: '${trigger.data.orderId}',
trackingNumber: '${createShippingLabel.response.trackingNumber}'
}
}
},
// Update order status
updateOrder: {
type: 'http',
method: 'PUT',
url: 'https://api.yourapp.com/orders/${trigger.data.orderId}',
body: {
status: 'shipped',
trackingNumber: '${createShippingLabel.response.trackingNumber}'
}
}
},
edges: [
// Start with value check
{ id: 'e1', source: 'trigger', sourceOutput: 'default', target: 'checkValue' },
// High value path
{ id: 'e2', source: 'checkValue', sourceOutput: 'true', target: 'flagForReview' },
{ id: 'e3', source: 'checkValue', sourceOutput: 'true', target: 'notifySales' },
// Standard path
{ id: 'e4', source: 'checkValue', sourceOutput: 'false', target: 'checkInventory' },
{ id: 'e5', source: 'checkInventory', sourceOutput: 'default', target: 'reserveInventory' },
{ id: 'e6', source: 'reserveInventory', sourceOutput: 'default', target: 'createShippingLabel' },
// After shipping label, do these in parallel
{ id: 'e7', source: 'createShippingLabel', sourceOutput: 'default', target: 'sendConfirmation' },
{ id: 'e8', source: 'createShippingLabel', sourceOutput: 'default', target: 'updateOrder' }
]
});
Best Practices
1. Use Meaningful Node IDs
// Good
nodes: {
trigger: { ... },
validateOrder: { ... },
checkInventory: { ... },
sendConfirmation: { ... }
}
// Bad
nodes: {
trigger: { ... },
node1: { ... },
node2: { ... },
node3: { ... }
}
2. Handle Errors
HTTP nodes fail if the response status is 4xx or 5xx. Design workflows to handle failures:
// Add error notification as a parallel path
edges: [
{ id: 'e1', source: 'apiCall', sourceOutput: 'default', target: 'nextStep' },
{ id: 'e2', source: 'apiCall', sourceOutput: 'error', target: 'notifyError' }
]
3. Set Timeouts
Prevent hanging workflows:
{
type: 'http',
method: 'POST',
url: 'https://slow-api.example.com/process',
timeoutMs: 30000 // 30 seconds
}
4. Start as Archived
Test workflows before activating:
const workflow = await sdk.workflow.createWorkflow({
key: 'my-workflow',
status: 'archived', // Test first; will not auto-trigger
// ...
});
// After testing
await sdk.workflow.updateWorkflow({
id: workflow.val.id,
status: 'ACTIVE'
});
Always test workflows in archived mode before activating them in production. Use the trigger endpoint to manually test with sample data.
Debugging Workflows
Manual Trigger for Testing
// Trigger with test data
const result = await sdk.workflow.triggerWorkflow({
secret: 'your_workflow_secret',
// Test payload
orderId: 'test_order_123',
customerEmail: 'test@example.com',
total: 9999,
items: [
{ productId: 'prod_1', quantity: 2 }
]
});
console.log('Execution result:', result);
Check Workflow Status
const workflows = await sdk.workflow.getWorkflows({
statuses: ['active'],
limit: 50
});
workflows.val.items.forEach(wf => {
console.log(wf.key, wf.status, wf.schedule || 'webhook-triggered');
});