Back to Arky

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 }
  ]
}
Tip

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,
});
Warning

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");
});