Workflows
Build powerful automations with DAG-based workflows
Workflows let you automate store 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 contacts 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.automation.workflow.create({
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",
},
timeout_ms: 30000,
delay_ms: 0,
retries: 0,
retry_delay_ms: 0,
body: {
text: "π New order from {{nodes.trigger.output.contact_email}}",
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: "*New Order*\nβ’ Contact: {{nodes.trigger.output.contact_email}}\nβ’ Total: {{nodes.trigger.output.total}} minor units ({{nodes.trigger.output.currency}})\nβ’ Items: {{nodes.trigger.output.item_count}}",
},
},
],
},
},
},
edges: [
{
source: "trigger",
output: "default",
target: "notifySlack",
back_edge: false,
},
],
});
console.log("Workflow created:", workflow.id);
Step 2: Configure Webhook in Your App
When an order is placed, trigger the workflow:
// In your order handler
async function handleNewOrder(order) {
const fullOrder = await sdk.eshop.order.get({ id: order.id });
const contact = await sdk.crm.contact.get({ id: order.contact_id });
// Save order to database
await saveOrder(fullOrder);
// Trigger the workflow
await sdk.automation.workflow.trigger({
secret: "wh_your_workflow_secret",
order_id: order.id,
contact_email: contact.email,
total: order.money.total,
currency: order.money.currency,
line_count: fullOrder.products.length + fullOrder.bookings.length,
});
}
To start the workflow from a store event, configure that event to call the workflowβs public trigger endpoint. The trigger node itself has no event selector:
trigger: {
type: "trigger";
}
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: 'https://api.example.com/step-1',
headers: {},
timeout_ms: 30000,
delay_ms: 0,
retries: 0,
retry_delay_ms: 0
},
step2: {
type: 'http',
method: 'post',
url: 'https://api.example.com/step-2',
headers: {},
timeout_ms: 30000,
delay_ms: 0,
retries: 0,
retry_delay_ms: 0
}
},
edges: [
{ source: 'trigger', output: 'default', target: 'step1', back_edge: false },
{ source: 'step1', output: 'success', target: 'step2', back_edge: false }
]
}
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',
headers: {},
timeout_ms: 30000,
delay_ms: 0,
retries: 0,
retry_delay_ms: 0
},
taskB: {
type: 'http',
method: 'post',
url: 'https://api.example.com/b',
headers: {},
timeout_ms: 30000,
delay_ms: 0,
retries: 0,
retry_delay_ms: 0
}
},
edges: [
{ source: 'trigger', output: 'default', target: 'taskA', back_edge: false },
{ source: 'trigger', output: 'default', target: 'taskB', back_edge: false }
]
}
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: 'switch',
rules: [{ condition: 'nodes.trigger.output.total > 10000' }] // Over 10,000 minor units
},
highValueFlow: {
type: 'http',
method: 'post',
url: 'https://hooks.slack.com/sales-team',
headers: {},
timeout_ms: 30000,
delay_ms: 0,
retries: 0,
retry_delay_ms: 0,
body: { text: 'π High-value order: {{nodes.trigger.output.total}} minor units ({{nodes.trigger.output.currency}})' }
},
standardFlow: {
type: 'http',
method: 'post',
url: 'https://api.example.com/process-standard',
headers: {},
timeout_ms: 30000,
delay_ms: 0,
retries: 0,
retry_delay_ms: 0
}
},
edges: [
{ source: 'trigger', output: 'default', target: 'checkAmount', back_edge: false },
{ source: 'checkAmount', output: 'rule_0', target: 'highValueFlow', back_edge: false },
{ source: 'checkAmount', output: 'fallback', target: 'standardFlow', back_edge: false }
]
}
Pattern 4: Delay and Read-only Retry
Add delays between steps. Retry settings apply only to read-only HTTP GET nodes; mutating HTTP
nodes and deploy webhooks make one durable, at-most-once provider attempt.
{
nodes: {
trigger: { type: 'trigger' },
submitJob: {
type: 'http',
method: 'post',
url: 'https://api.example.com/jobs',
headers: {},
timeout_ms: 30000,
delay_ms: 0,
retries: 0,
retry_delay_ms: 0,
body: { /* job input */ }
},
checkJob: {
type: 'http',
method: 'get',
url: 'https://api.example.com/jobs/latest',
headers: {},
timeout_ms: 30000,
delay_ms: 10000,
retries: 2,
retry_delay_ms: 10000
}
},
edges: [
{ source: 'trigger', output: 'default', target: 'submitJob', back_edge: false },
{ source: 'submitJob', output: 'success', target: 'checkJob', back_edge: false }
]
}
Scheduled Workflows
Run workflows on a schedule using cron expressions.
const dailyReport = await sdk.automation.workflow.create({
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",
headers: {},
timeout_ms: 30000,
delay_ms: 0,
retries: 0,
retry_delay_ms: 0,
},
sendReport: {
type: "http",
method: "post",
url: "https://hooks.slack.com/services/xxx",
headers: {},
timeout_ms: 30000,
delay_ms: 0,
retries: 0,
retry_delay_ms: 0,
body: {
text: "π Daily Sales Report\nTotal: {{input.fetchSales.body.total}} minor units ({{input.fetchSales.body.currency}})\nOrders: {{input.fetchSales.body.orderCount}}",
},
},
},
edges: [
{
source: "trigger",
output: "default",
target: "fetchSales",
back_edge: false,
},
{
source: "fetchSales",
output: "success",
target: "sendReport",
back_edge: false,
},
],
});
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
Interpolate values with {{ ... }}. Conditions and transform code use JavaScript expressions
without braces.
Available Variables
| Variable | Description |
| --------------------------- | --------------------------------------------------------- |
| {{input.sourceNodeId.*}} | Output from a node connected directly to the current node |
| {{nodes.nodeId.output.*}} | Output from any completed node |
Example: Chaining API Calls
{
nodes: {
trigger: { type: 'trigger' },
// First: Get contact details
getContact: {
type: 'http',
method: 'get',
url: 'https://api.crm.com/contacts/{{nodes.trigger.output.contactId}}',
headers: {
'Authorization': 'Bearer YOUR_CRM_API_KEY'
},
timeout_ms: 30000,
delay_ms: 0,
retries: 0,
retry_delay_ms: 0
},
// Second: Use contact data to create ticket
createTicket: {
type: 'http',
method: 'post',
url: 'https://api.support.com/tickets',
headers: {
'Authorization': 'Bearer YOUR_SUPPORT_API_KEY'
},
timeout_ms: 30000,
delay_ms: 0,
retries: 0,
retry_delay_ms: 0,
body: {
contact_name: '{{input.getContact.body.name}}',
contact_email: '{{input.getContact.body.email}}',
issue: '{{nodes.trigger.output.issue_description}}'
}
}
},
edges: [
{ source: 'trigger', output: 'default', target: 'getContact', back_edge: false },
{ source: 'getContact', output: 'success', target: 'createTicket', back_edge: false }
]
}
Complete Example: Order Fulfillment
A real-world workflow that handles order processing:
const fulfillmentWorkflow = await sdk.automation.workflow.create({
key: "order-fulfillment",
status: "active",
nodes: {
trigger: {
type: "trigger",
},
// Check if high-value order
checkValue: {
type: "switch",
rules: [{ condition: "nodes.trigger.output.total > 50000" }], // Over 50,000 minor units
},
// High value: Manual review required
flagForReview: {
type: "http",
method: "post",
url: "https://api.yourapp.com/orders/{{nodes.trigger.output.orderId}}/flag",
headers: {},
timeout_ms: 30000,
delay_ms: 0,
retries: 0,
retry_delay_ms: 0,
body: { reason: "high_value", requires_approval: true },
},
// Notify sales team about high-value order
notifySales: {
type: "http",
method: "post",
url: "https://hooks.slack.com/services/sales",
headers: {},
timeout_ms: 30000,
delay_ms: 0,
retries: 0,
retry_delay_ms: 0,
body: {
text: "π° High-value order needs review: {{nodes.trigger.output.total}} minor units ({{nodes.trigger.output.currency}})",
},
},
// Standard flow: Check inventory
checkInventory: {
type: "http",
method: "post",
url: "https://api.inventory.com/check",
headers: {},
timeout_ms: 30000,
delay_ms: 0,
retries: 0,
retry_delay_ms: 0,
body: { items: "{{nodes.trigger.output.items}}" },
},
// Reserve inventory
reserveInventory: {
type: "http",
method: "post",
url: "https://api.inventory.com/reserve",
headers: {},
timeout_ms: 30000,
delay_ms: 0,
retries: 0,
retry_delay_ms: 0,
body: {
order_id: "{{nodes.trigger.output.orderId}}",
items: "{{nodes.trigger.output.items}}",
},
},
// Generate shipping label
createShippingLabel: {
type: "http",
method: "post",
url: "https://api.shipping.com/labels",
headers: {},
timeout_ms: 30000,
delay_ms: 0,
retries: 0,
retry_delay_ms: 0,
body: {
order_id: "{{nodes.trigger.output.orderId}}",
address: "{{nodes.trigger.output.shippingAddress}}",
weight: "{{nodes.checkInventory.output.body.totalWeight}}",
},
},
// Send confirmation email
sendConfirmation: {
type: "http",
method: "post",
url: "https://api.email.com/send",
headers: {},
timeout_ms: 30000,
delay_ms: 0,
retries: 0,
retry_delay_ms: 0,
body: {
to: "{{nodes.trigger.output.contactEmail}}",
template: "order_shipped",
data: {
order_id: "{{nodes.trigger.output.orderId}}",
tracking_number:
"{{nodes.createShippingLabel.output.body.trackingNumber}}",
},
},
},
},
edges: [
// Start with value check
{
source: "trigger",
output: "default",
target: "checkValue",
back_edge: false,
},
// High value path
{
source: "checkValue",
output: "rule_0",
target: "flagForReview",
back_edge: false,
},
{
source: "checkValue",
output: "rule_0",
target: "notifySales",
back_edge: false,
},
// Standard path
{
source: "checkValue",
output: "fallback",
target: "checkInventory",
back_edge: false,
},
{
source: "checkInventory",
output: "success",
target: "reserveInventory",
back_edge: false,
},
{
source: "reserveInventory",
output: "success",
target: "createShippingLabel",
back_edge: false,
},
// Send the confirmation after the label is created
{
source: "createShippingLabel",
output: "success",
target: "sendConfirmation",
back_edge: false,
},
],
});
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: [
{
source: "apiCall",
output: "success",
target: "nextStep",
back_edge: false,
},
{
source: "apiCall",
output: "error",
target: "notifyError",
back_edge: false,
},
];
3. Set Timeouts
Prevent hanging workflows:
{
type: 'http',
method: 'post',
url: 'https://slow-api.example.com/process',
headers: {},
timeout_ms: 30000, // 30 seconds
delay_ms: 0,
retries: 0,
retry_delay_ms: 0
}
4. Activate Deliberately
Only active workflows run from events, schedules, or the public trigger endpoint. Create the
definition as draft, inspect it, then submit the full definition with status: 'active' before
testing the trigger.
const draft = await sdk.automation.workflow.create({
key: "my-workflow",
status: "draft",
nodes,
edges,
});
const workflow = await sdk.automation.workflow.update({
id: draft.id,
key: draft.key,
status: "active",
nodes: draft.nodes,
edges: draft.edges,
schedule: draft.schedule,
});
Triggering a workflow creates a new execution. Do not put a trigger call in a generic retry loop: a new execution can create new external effects.
Debugging Workflows
Manual Trigger for Testing
// Trigger with test data
const execution = await sdk.automation.workflow.trigger({
secret: "your_workflow_secret",
// Test payload
order_id: "test_order_123",
contact_email: "test@example.com",
total: 9999,
items: [{ product_id: "prod_1", quantity: 2 }],
});
console.log("Execution result:", execution);
Check Workflow Status
const workflows = await sdk.automation.workflow.find({
status: "active",
limit: 50,
});
workflows.items.forEach((wf) => {
console.log(wf.key, wf.status, wf.schedule || "webhook-triggered");
});