Your First Workflow

Build a working workflow from scratch: register an action, create a matcher with conditional branching, execute it with sample data, add human approval, and read the final output.

Prerequisites

A running Hyphen instance at http://localhost:3009. All examples use X-Org-Id: tutorial-org.


Step 1: Register a Mock HTTP Action

Before building the workflow, register an action the workflow can call. This simulates updating an ERP system:

bash
curl -X POST http://localhost:3009/actions \
  -H "X-Org-Id: tutorial-org" \
  -H "Content-Type: application/json" \
  -d '{
    "action_name": "update_erp_status",
    "kind": "http",
    "description": "Mark a record as reconciled in the ERP",
    "url": "https://httpbin.org/post",
    "http_method": "POST",
    "passthrough": true
  }'

We're using httpbin.org as a mock endpoint. In production, this would be your actual ERP API.

Verify it was registered:
bash
curl http://localhost:3009/actions \
  -H "X-Org-Id: tutorial-org"

Step 2: Create the Workflow

Create a workflow that matches invoices to payments and conditionally processes the results:

bash
curl -X POST http://localhost:3009/workflows \
  -H "X-Org-Id: tutorial-org" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "first_workflow",
    "definition": {
      "actions": [
        {
          "type": "matcher",
          "properties": {
            "left": "@input.invoices",
            "right": "@input.payments",
            "matchOn": ["invoice_id"],
            "tolerance": 0.05,
            "outputMatched": "matched",
            "outputUnmatchedLeft": "unmatched_invoices",
            "outputUnmatchedRight": "unmatched_payments"
          }
        },
        {
          "type": "update_erp_status",
          "filter": {
            "condition": { "greaterThan": [{ "length": "@matched" }, 0] }
          },
          "properties": {
            "records": "@matched",
            "action": "mark_reconciled"
          }
        }
      ]
    }
  }'

Save the returned workflow ID — you'll need it to execute.

What this does: Takes two datasets (invoices and payments), matches them on invoice_id with 5% amount tolerance, then calls the update_erp_status action if any matches were found.


Step 3: Execute with Sample Data

bash
curl -X POST http://localhost:3009/workflows/WORKFLOW_ID/execute \
  -H "X-Org-Id: tutorial-org" \
  -H "Content-Type: application/json" \
  -d '{
    "invoices": [
      { "invoice_id": "INV-001", "vendor": "Acme Corp", "amount": 1000.00 },
      { "invoice_id": "INV-002", "vendor": "Beta LLC", "amount": 2500.00 },
      { "invoice_id": "INV-003", "vendor": "Gamma Inc", "amount": 500.00 }
    ],
    "payments": [
      { "invoice_id": "INV-001", "vendor": "Acme Corp", "amount": 1000.00 },
      { "invoice_id": "INV-002", "vendor": "Beta LLC", "amount": 2480.00 }
    ]
  }'

Replace WORKFLOW_ID with the ID returned in Step 2.


Step 4: Read the Run Output

bash
curl http://localhost:3009/runs/RUN_ID/status \
  -H "X-Org-Id: tutorial-org"

Expected output:

json
{
  "status": "completed",
  "context": {
    "matched": [
      { "a": { "invoice_id": "INV-001", "amount": 1000.00 }, "b": { "invoice_id": "INV-001", "amount": 1000.00 } },
      { "a": { "invoice_id": "INV-002", "amount": 2500.00 }, "b": { "invoice_id": "INV-002", "amount": 2480.00 } }
    ],
    "unmatched_invoices": [
      { "invoice_id": "INV-003", "vendor": "Gamma Inc", "amount": 500.00 }
    ],
    "unmatched_payments": []
  }
}

INV-001 matched exactly. INV-002 matched within the 5% tolerance ($2500 vs $2480 = 0.8% difference). INV-003 had no matching payment, so it appears in unmatched_invoices.


Step 5: Add a PbotApproval Step

Update the workflow to require human approval when there are unmatched invoices:

bash
curl -X PUT http://localhost:3009/workflows/WORKFLOW_ID \
  -H "X-Org-Id: tutorial-org" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "first_workflow_v2",
    "definition": {
      "actions": [
        {
          "type": "matcher",
          "properties": {
            "left": "@input.invoices",
            "right": "@input.payments",
            "matchOn": ["invoice_id"],
            "tolerance": 0.05,
            "outputMatched": "matched",
            "outputUnmatchedLeft": "unmatched_invoices",
            "outputUnmatchedRight": "unmatched_payments"
          }
        },
        {
          "type": "update_erp_status",
          "filter": {
            "condition": { "greaterThan": [{ "length": "@matched" }, 0] }
          },
          "properties": {
            "records": "@matched",
            "action": "mark_reconciled"
          }
        },
        {
          "type": "PbotApproval",
          "filter": {
            "condition": { "greaterThan": [{ "length": "@unmatched_invoices" }, 0] }
          },
          "properties": {
            "comment": "{{unmatched_invoices.length}} invoices have no matching payment. Review and decide.",
            "request_payload": {
              "unmatched": "@unmatched_invoices",
              "matched_count": "@matched.length"
            }
          }
        }
      ]
    }
  }'

Execute it again with the same data. This time the run will pause at the approval step.


Step 6: Submit Approval

Check the run status — it should show paused:

bash
curl http://localhost:3009/runs/RUN_ID/status \
  -H "X-Org-Id: tutorial-org"

Submit the approval:

bash
curl -X POST http://localhost:3009/approvals/RUN_ID/2 \
  -H "X-Org-Id: tutorial-org" \
  -H "Content-Type: application/json" \
  -d '{
    "approved": true,
    "comments": "INV-003 is a known timing issue. Will resolve next cycle.",
    "data": { "reviewer": "tutorial-user" }
  }'

The /2 in the URL refers to step index 2 (the third step, zero-indexed).


Step 7: Check Final State

bash
curl http://localhost:3009/runs/RUN_ID/status \
  -H "X-Org-Id: tutorial-org"

The run should now show completed with the approval decision captured:

json
{
  "status": "completed",
  "context": {
    "matched": [ ... ],
    "unmatched_invoices": [ ... ],
    "__approved": true,
    "__approval_data": {
      "reviewer": "tutorial-user",
      "comments": "INV-003 is a known timing issue."
    }
  }
}

What You Built

A workflow that:

  1. Matches invoices to payments on invoice_id with 5% amount tolerance
  2. Calls an external action to mark matched records as reconciled
  3. Pauses for human review when unmatched invoices exist
  4. Captures the reviewer's decision as part of the permanent audit trail

Next: Your First Agent — add an AI agent that investigates the unmatched exceptions.