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:
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.
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:
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
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
curl http://localhost:3009/runs/RUN_ID/status \
-H "X-Org-Id: tutorial-org"
Expected output:
{
"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:
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:
curl http://localhost:3009/runs/RUN_ID/status \
-H "X-Org-Id: tutorial-org"
Submit the approval:
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
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:
{
"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:
- Matches invoices to payments on
invoice_idwith 5% amount tolerance - Calls an external action to mark matched records as reconciled
- Pauses for human review when unmatched invoices exist
- 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.