15 minlesson

Building an Invoice Generation Workflow

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:

  1. Fetches the order and validates it exists
  2. Recalculates charges in case rates changed
  3. Generates a sequential invoice number
  4. Creates an Invoice accounting transaction
  5. Attaches the invoice number to the transaction
  6. Creates a payment if the order is configured for auto-pay
  7. Sends invoice details to an external ERP via HTTP
  8. Caches the invoice ID for subsequent lookups
  9. 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.

yaml
1- task: "Order/Get@1"
2 name: GetOrder
3 inputs:
4 orderId: "{{ inputs.orderId }}"
5 outputs:
6 - name: order
7 mapping: "order"
8
9- task: "Utilities/Error@1"
10 name: ValidateOrder
11 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:

yaml
1- task: "Order/RecalculateCharges@1"
2 name: Recalculate
3 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:

yaml
1- task: "Number/Generate@1"
2 name: GenInvoiceNumber
3 inputs:
4 format: "INV-{0:D6}"
5 sequenceName: "invoice"
6 outputs:
7 - name: number
8 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:

yaml
1- task: "AccountingTransaction/Generate@1"
2 name: GenerateInvoice
3 inputs:
4 orderId: "{{ inputs.orderId }}"
5 transactionType: "Invoice"
6 outputs:
7 - name: invoice
8 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:

yaml
1- task: "AccountingTransaction/Update@1"
2 name: AttachNumber
3 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:

yaml
1- task: "Payment/Create@1"
2 name: CreatePayment
3 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: payment
11 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:

yaml
1- task: "Utilities/HttpRequest@1"
2 name: NotifyErp
3 inputs:
4 actionEvents:
5 enabled: true
6 eventName: "erp.invoiceCreated"
7 eventDataExt:
8 orderId: "{{ inputs.orderId }}"
9 url: "{{ erpConfig.baseUrl }}/api/invoices"
10 method: POST
11 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: erpResult
22 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:

yaml
1- task: "Caching/SetCache@1"
2 name: CacheInvoice
3 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:

yaml
1onActivityFailed:
2 - task: "Utilities/Log@1"
3 name: LogFailure
4 inputs:
5 message: "Invoice generation failed for order {{ inputs.orderId }}: {{ error.message }}"
6 level: Error
7
8 - task: "ActionEvent/Create"
9 name: AlertOpsTeam
10 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 → GenInvoiceNumber
2 → 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:

  1. Loads and validates the order
  2. Recalculates charges with current rates
  3. Generates a zero-padded sequential invoice number using Number/Generate
  4. Creates an Invoice accounting transaction with AccountingTransaction/Generate
  5. Attaches the number and transitions status with AccountingTransaction/Update
  6. Conditionally charges the customer with Payment/Create
  7. Notifies an external ERP with Utilities/HttpRequest and actionEvents
  8. Caches the invoice ID with Caching/SetCache for 24 hours
  9. Handles failures gracefully with onActivityFailed logging and event alerts