Building an Invoice Generation Workflow
This lesson walks through building a complete invoice generation workflow from scratch. The workflow queries an order and its charges, generates a sequential invoice number, creates the accounting transaction, optionally processes payment, notifies an external ERP system via HTTP, caches the result, and handles errors. By the end you will understand how all the accounting, payment, and utility tasks connect in a realistic business process.
What the Workflow Does
Given an orderId input, the workflow:
- Fetches the order and validates it exists
- Recalculates charges in case rates changed
- Generates a sequential invoice number
- Creates an Invoice accounting transaction
- Attaches the invoice number to the transaction
- Creates a payment if the order is configured for auto-pay
- Sends invoice details to an external ERP via HTTP
- Caches the invoice ID for subsequent lookups
- Handles failures at any step with
onActivityFailed
Step 1: Fetch and Validate the Order
Always start by loading the order and confirming it exists before committing financial records.
yaml1- task: "Order/Get@1"2 name: GetOrder3 inputs:4 orderId: "{{ inputs.orderId }}"5 outputs:6 - name: order7 mapping: "order"89- task: "Utilities/Error@1"10 name: ValidateOrder11 conditions:12 - expression: "isNullOrEmpty([Data?.GetOrder?.order?]) = true"13 inputs:14 message: "Order not found: {{ inputs.orderId }}"
If the order does not exist, ValidateOrder throws a workflow error and stops execution. No financial records are created for a missing order.
Step 2: Recalculate Charges
Rates may have changed since the order was created. Recalculating ensures the invoice reflects current pricing:
yaml1- task: "Order/RecalculateCharges@1"2 name: Recalculate3 inputs:4 orderId: "{{ inputs.orderId }}"
This replaces all auto-calculated charges. Manually entered charges are preserved.
Step 3: Generate the Invoice Number
Obtain a unique, sequential invoice number before creating the accounting transaction so the number can be attached immediately:
yaml1- task: "Number/Generate@1"2 name: GenInvoiceNumber3 inputs:4 format: "INV-{0:D6}"5 sequenceName: "invoice"6 outputs:7 - name: number8 mapping: "number"
INV-{0:D6} zero-pads the counter to six digits: INV-000001, INV-000002, and so on. The counter increments atomically — no two workflow executions will receive the same number.
Step 4: Generate the Invoice Transaction
Create the accounting document. All charges associated with the order are automatically included in the total:
yaml1- task: "AccountingTransaction/Generate@1"2 name: GenerateInvoice3 inputs:4 orderId: "{{ inputs.orderId }}"5 transactionType: "Invoice"6 outputs:7 - name: invoice8 mapping: "transaction"
The output invoice contains id, total, status, and other fields. The invoice is created in Draft status.
Step 5: Attach the Invoice Number and Mark Sent
Update the accounting transaction to set the human-readable reference number and transition it to Sent status:
yaml1- task: "AccountingTransaction/Update@1"2 name: AttachNumber3 inputs:4 transactionId: "{{ Data.GenerateInvoice.invoice.id }}"5 entity:6 referenceNumber: "{{ Data.GenInvoiceNumber.number }}"7 status: "Sent"
Step 6: Create a Payment (Conditional Auto-Pay)
If the order's custom field autoCharge is true, immediately create a payment:
yaml1- task: "Payment/Create@1"2 name: CreatePayment3 conditions:4 - expression: "[Data?.GetOrder?.order?.customValues?.autoCharge?] = true"5 inputs:6 orderId: "{{ inputs.orderId }}"7 amount: "{{ Data.GenerateInvoice.invoice.total }}"8 paymentMethod: "{{ Data.GetOrder.order.customValues.paymentMethod }}"9 outputs:10 - name: payment11 mapping: "payment"
The condition prevents charging customers who have not opted into automatic billing.
Step 7: Notify the External ERP
After the invoice is finalized, send a notification to the external ERP system. Use actionEvents so the request appears in the order's activity log:
yaml1- task: "Utilities/HttpRequest@1"2 name: NotifyErp3 inputs:4 actionEvents:5 enabled: true6 eventName: "erp.invoiceCreated"7 eventDataExt:8 orderId: "{{ inputs.orderId }}"9 url: "{{ erpConfig.baseUrl }}/api/invoices"10 method: POST11 contentType: "application/json"12 headers:13 - name: "Authorization"14 value: "Bearer {{ erpConfig.apiToken }}"15 body:16 invoiceNumber: "{{ Data.GenInvoiceNumber.number }}"17 invoiceId: "{{ Data.GenerateInvoice.invoice.id }}"18 orderId: "{{ inputs.orderId }}"19 total: "{{ Data.GenerateInvoice.invoice.total }}"20 outputs:21 - name: erpResult22 mapping: "response?.body?"
If the ERP call fails, the workflow continues to the next step by default. If a failed ERP notification should halt the workflow, add continueOnError: false (default behaviour — the workflow already stops on unhandled errors). To log and continue, use onActivityFailed.
Step 8: Cache the Invoice ID
Store the invoice ID in the cache so other workflows (e.g., a payment reminder workflow) can look it up without querying the database:
yaml1- task: "Caching/SetCache@1"2 name: CacheInvoice3 inputs:4 key: "invoice-order-{{ inputs.orderId }}"5 value: "{{ Data.GenerateInvoice.invoice.id }}"6 ttl: 86400
ttl: 86400 expires the entry after 24 hours — appropriate for invoices that should be looked up the same business day.
Step 9: Error Handling with onActivityFailed
The onActivityFailed hook runs when any named step fails. Use it to log the error or trigger a compensating action:
yaml1onActivityFailed:2 - task: "Utilities/Log@1"3 name: LogFailure4 inputs:5 message: "Invoice generation failed for order {{ inputs.orderId }}: {{ error.message }}"6 level: Error78 - task: "ActionEvent/Create"9 name: AlertOpsTeam10 inputs:11 eventName: "billing.invoiceGenerationFailed"12 data:13 orderId: "{{ inputs.orderId }}"14 errorMessage: "{{ error.message }}"
This sends a platform event that can trigger a notification to the operations team without requiring a hard failure of the parent process.
Complete Workflow Summary
The full workflow connects these tasks in sequence:
1GetOrder → ValidateOrder → Recalculate → GenInvoiceNumber2 → GenerateInvoice → AttachNumber → CreatePayment (conditional)3 → NotifyErp → CacheInvoice
Key design principles applied:
- Validate early — fail fast before any financial records are created
- Recalculate before invoicing — ensure charges reflect current rates
- Generate the number first — atomic counter prevents duplicates
- Conditional payment — check custom values before charging
- actionEvents — link external calls to the order's audit trail
- Cache the result — reduce database queries for downstream workflows
- onActivityFailed — log and alert without hiding failures
Summary
In this lesson you built an invoice generation workflow that:
- Loads and validates the order
- Recalculates charges with current rates
- Generates a zero-padded sequential invoice number using
Number/Generate - Creates an Invoice accounting transaction with
AccountingTransaction/Generate - Attaches the number and transitions status with
AccountingTransaction/Update - Conditionally charges the customer with
Payment/Create - Notifies an external ERP with
Utilities/HttpRequestandactionEvents - Caches the invoice ID with
Caching/SetCachefor 24 hours - Handles failures gracefully with
onActivityFailedlogging and event alerts