Back to arky.io

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 CaseExample
Event reactionsSend Slack notification when order placed
Data syncSync new customers to your CRM
Scheduled tasksGenerate daily reports at 9am
Multi-step processesOrder fulfillment with inventory check, shipping label, notification
Conditional logicDifferent 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' }
  ]
}
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: '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

ExpressionSchedule
0 9 * * *Every day at 9:00 AM
0 */2 * * *Every 2 hours
0 9 * * 1Every Monday at 9:00 AM
0 0 1 * *First day of every month
*/15 * * * *Every 15 minutes
0 9 * * 1-5Weekdays at 9:00 AM

Using Variables

Access data from trigger input and previous nodes using ${...} syntax.

Available Variables

VariableDescription
${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'
});
Warning

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