Workflows
Automation workflows with DAG-based execution, scheduling, and webhooks
Workflows enable powerful automation with a DAG (Directed Acyclic Graph) execution model. Unlike simple sequential workflows, Arky workflows can run nodes in parallel, branch conditionally, and trigger from webhooks or schedules.
Key Concepts
Workflow Structure
A workflow consists of nodes and edges:
- Nodes: Individual operations (trigger, HTTP request, deploy webhook, switch, transform, loop)
- Edges: Connections between nodes that define execution flow
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Trigger │────▶│ HTTP │────▶│ HTTP │
└─────────┘ │ (fetch) │ │ (notify)│
└─────────┘ └─────────┘
Node Types
| Type | Description |
| --------------------- | ------------------------------------------------------------ |
| trigger | Entry point - webhook or scheduled |
| http | Make HTTP requests to external APIs |
| send_email | Send a typed email through a configured mailbox and template |
| deploy_webhook | Call a store build hook endpoint |
| google_drive_upload | Upload content through a Google Drive workflow connection |
| switch | Branch based on expressions |
| transform | Transform data with JavaScript |
| loop | Iterate over arrays |
Create Workflow
/v1/stores/{storeId}/workflows sdk.automation.workflow.create() Create a new automation workflow.
const workflow = await sdk.automation.workflow.create({
key: 'order-notification',
status: 'active',
nodes: {
trigger: {
type: 'trigger'
},
fetchOrder: {
type: 'http',
method: 'get',
url: 'https://api.yourapp.com/orders/{{input.trigger.orderId}}',
headers: {
'Authorization': 'Bearer YOUR_API_KEY'
},
timeout_ms: 30000,
delay_ms: 0,
retries: 0,
retry_delay_ms: 0
},
sendSlack: {
type: 'http',
method: 'post',
url: 'https://hooks.slack.com/services/xxx',
headers: { 'Content-Type': 'application/json' },
body: {
text: 'New order #{{input.fetchOrder.body.orderNumber}}, total {{input.fetchOrder.body.total}} minor units ({{input.fetchOrder.body.currency}})'
},
timeout_ms: 30000,
delay_ms: 0,
retries: 0,
retry_delay_ms: 0
}
},
edges: [
{ source: 'trigger', output: 'default', target: 'fetchOrder', back_edge: false },
{ source: 'fetchOrder', output: 'success', target: 'sendSlack', back_edge: false }
]
});Parameters
| Name | Type | Description |
|---|---|---|
key required | string | Unique workflow identifier |
status optional | active | draft | archived | Workflow status (default: active) |
nodes required | Record<string, WorkflowNode> | Map of node IDs to node definitions |
edges required | WorkflowEdge[] | Connections between nodes |
schedule optional | string | Cron expression for scheduled execution (e.g., '0 9 * * *' for 9am daily) |
Each edge has exactly source, target, output, and back_edge. Use default after a trigger,
success or error after an HTTP/effect node, rule_0, rule_1, and so on plus fallback after a
switch, and each or done after a loop. Set back_edge: false except for an explicit edge that
returns a loop body to its loop node.
Get Workflow
/v1/stores/{storeId}/workflows/{id} sdk.automation.workflow.get() Retrieve a workflow by ID.
const workflow = await sdk.automation.workflow.get({
id: "wf_abc123",
});
console.log(workflow.key, workflow.status);
console.log("Nodes:", Object.keys(workflow.nodes));
List Workflows
/v1/stores/{storeId}/workflows sdk.automation.workflow.find() List workflows with filtering and pagination.
const { items, cursor } = await sdk.automation.workflow.find({
status: "active",
query: "notification",
limit: 20,
cursor: null,
});
items.forEach((workflow) => {
console.log(workflow.key, workflow.status);
});
Parameters
| Name | Type | Description |
|---|---|---|
ids optional | string[] | Filter by specific workflow IDs |
status optional | active | draft | archived | Filter by workflow status |
query optional | string | Search in workflow keys |
limit optional | number | Items per page (max 100) |
cursor optional | string | Pagination cursor |
sort_field optional | string | Sort field (createdAt, key) |
sort_direction optional | asc | desc | Sort direction |
Update Workflow
/v1/stores/{storeId}/workflows/{id} sdk.automation.workflow.update() Update an existing workflow.
const workflow = await sdk.automation.workflow.update({
id: "wf_abc123",
key: "order-notification-v2",
status: "active",
nodes: {
// Updated node definitions
trigger: { type: "trigger" },
notify: {
type: "http",
method: "post",
url: "https://api.example.com/notify",
headers: {},
timeout_ms: 30000,
delay_ms: 0,
retries: 0,
retry_delay_ms: 0,
},
},
edges: [
{
source: "trigger",
output: "default",
target: "notify",
back_edge: false,
},
],
});
Delete Workflow
/v1/stores/{storeId}/workflows/{id} sdk.automation.workflow.delete() Delete a workflow.
const deleted = await sdk.automation.workflow.delete({
id: "wf_abc123",
});
Trigger Workflow
/v1/workflows/trigger/{secret} sdk.automation.workflow.trigger() Trigger a workflow execution via webhook. This endpoint does not require authentication - the secret in the URL validates the request.
// The secret comes from your workflow's webhook URL
const execution = await sdk.automation.workflow.trigger({
secret: 'wh_secret_abc123xyz',
// Pass any data to the workflow
orderId: 'ord_123',
contact_email: 'contact@example.com',
items: [
{ product_id: 'prod_1', quantity: 2 }
]
});Parameters
| Name | Type | Description |
|---|---|---|
secret required | string | Workflow webhook secret from the trigger URL |
[key: string] optional | unknown | Additional JSON-object properties passed to the trigger |
The request body must be a JSON object. A node connected directly to a trigger
node named trigger reads orderId as input.trigger.orderId; any later
node can use nodes.trigger.output.orderId after the trigger has completed.
List Executions
/v1/stores/{storeId}/workflows/{workflowId}/executions sdk.automation.workflow.getExecutions() List past executions of a workflow.
const { items, cursor } = await sdk.automation.workflow.getExecutions({
workflow_id: "wf_abc123",
limit: 20,
});
items.forEach((execution) => {
console.log(execution.id, execution.status, execution.started_at);
});
Parameters
| Name | Type | Description |
|---|---|---|
workflow_id required | string | Workflow ID |
limit optional | number | Items per page |
cursor optional | string | Pagination cursor |
Get Execution
/v1/stores/{storeId}/workflows/{workflowId}/executions/{executionId} sdk.automation.workflow.getExecution() Retrieve a single execution including node-level state and any errors.
const execution = await sdk.automation.workflow.getExecution({
workflow_id: "wf_abc123",
execution_id: "exec_xyz789",
});
console.log(execution.status); // pending | running | completed | failed | cancelled
console.log(execution.results); // Record<nodeId, NodeResult>
Parameters
| Name | Type | Description |
|---|---|---|
workflow_id required | string | Workflow ID |
execution_id required | string | Execution ID |
Node Types Reference
Trigger Node
The entry point for workflow execution. Every workflow must have exactly one trigger node.
{
type: 'trigger',
delay_ms: 0
}
HTTP Node
Make HTTP requests to external APIs. Supports template variables from previous nodes.
{
type: 'http',
method: 'post', // get, post, put, patch, delete
url: 'https://api.example.com/endpoint',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: {
userId: '{{input.trigger.userId}}',
message: 'Hello from Arky!'
},
timeout_ms: 30000,
delay_ms: 0,
retries: 0,
retry_delay_ms: 0
}
Available Variables:
{{input.sourceNodeId.*}}- Output from a node connected directly to this node{{nodes.nodeId.output.*}}- Output from any completed node
Switch Node
Conditional branching based on JavaScript expressions.
{
type: 'switch',
rules: [{ condition: 'input.trigger.amount > 100' }]
}
Switch nodes use one output per rule plus a fallback output:
rule_0- Executed when the first rule matchesfallback- Executed when no rule matches
edges: [
{
source: "checkAmount",
output: "rule_0",
target: "sendHighValueAlert",
back_edge: false,
},
{
source: "checkAmount",
output: "fallback",
target: "sendStandardNotification",
back_edge: false,
},
];
Delays And Read-only Retries
HTTP and deploy webhook nodes support delay_ms. Retry settings apply only to read-only HTTP
GET nodes. Mutating HTTP methods, deploy webhooks, uploads, and other external effects make one
durably claimed provider attempt; an ambiguous result becomes terminal unknown instead of being
sent again.
{
type: 'http',
method: 'get',
url: 'https://api.example.com/jobs/latest',
headers: {},
timeout_ms: 30000,
delay_ms: 300000,
retries: 2,
retry_delay_ms: 10000
}
Scheduled Workflows
Run workflows on a schedule using cron expressions.
const workflow = await sdk.automation.workflow.create({
key: "daily-report",
status: "active",
schedule: "0 9 * * *", // Every day at 9:00 AM UTC
nodes: {
trigger: { type: "trigger" },
generateReport: {
type: "http",
method: "post",
url: "https://api.yourapp.com/reports/generate",
headers: {},
timeout_ms: 30000,
delay_ms: 0,
retries: 0,
retry_delay_ms: 0,
},
sendEmail: {
type: "http",
method: "post",
url: "https://api.yourapp.com/email/send",
body: {
to: "team@company.com",
subject: "Daily Report",
body: "{{input.generateReport.body.reportUrl}}",
},
headers: {},
timeout_ms: 30000,
delay_ms: 0,
retries: 0,
retry_delay_ms: 0,
},
},
edges: [
{
source: "trigger",
output: "default",
target: "generateReport",
back_edge: false,
},
{
source: "generateReport",
output: "success",
target: "sendEmail",
back_edge: false,
},
],
});
Common Cron Expressions:
| Expression | Description |
| -------------- | ------------------------ |
| 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 |
Complete Example: Order Processing
const workflow = await sdk.automation.workflow.create({
key: "process-new-order",
status: "active",
nodes: {
// Entry point
trigger: {
type: "trigger",
},
// Check order value
checkValue: {
type: "switch",
rules: [{ condition: "input.trigger.total > 10000" }], // Over 10,000 minor units
},
// High value: notify sales team
notifySales: {
type: "http",
method: "post",
url: "https://hooks.slack.com/services/sales-channel",
body: {
text: "🎉 High-value order! {{nodes.trigger.output.total}} minor units ({{nodes.trigger.output.currency}}) from {{nodes.trigger.output.contactEmail}}",
},
headers: {},
timeout_ms: 30000,
delay_ms: 0,
retries: 0,
retry_delay_ms: 0,
},
// Always: update inventory
updateInventory: {
type: "http",
method: "post",
url: "https://api.yourapp.com/inventory/decrement",
body: {
items: "{{input.trigger.items}}",
},
headers: {},
timeout_ms: 30000,
delay_ms: 0,
retries: 0,
retry_delay_ms: 0,
},
// Send confirmation email
sendConfirmation: {
type: "http",
method: "post",
url: "https://api.yourapp.com/email/order-confirmation",
body: {
orderId: "{{nodes.trigger.output.orderId}}",
email: "{{nodes.trigger.output.contactEmail}}",
},
headers: {},
timeout_ms: 30000,
delay_ms: 0,
retries: 0,
retry_delay_ms: 0,
},
},
edges: [
// Trigger -> Check Value
{
source: "trigger",
output: "default",
target: "checkValue",
back_edge: false,
},
// High value -> Notify Sales
{
source: "checkValue",
output: "rule_0",
target: "notifySales",
back_edge: false,
},
// Both paths -> Update Inventory (runs in parallel with notification)
{
source: "trigger",
output: "default",
target: "updateInventory",
back_edge: false,
},
// After inventory -> Send Confirmation
{
source: "updateInventory",
output: "success",
target: "sendConfirmation",
back_edge: false,
},
],
});
Workflows execute nodes in parallel when possible. In the example above,
checkValue and updateInventory run simultaneously since they both connect
directly to the trigger.