15 minlesson

Invoice Document Workflow

Invoice Document Workflow

This lesson walks through building a complete invoice generation workflow from scratch. By the end you will have a workflow that queries an order with its charges, renders a PDF invoice, stores it as an attachment on the order, and emails it to the customer. This pattern is the foundation for all document generation workflows in CXTMS.

What You Will Build

A workflow named generate-invoice that:

  1. Accepts an orderId as input
  2. Queries the order with its charges and customer details
  3. Renders a PDF invoice using Handlebars and the chrome-pdf recipe
  4. Creates an attachment on the order entity
  5. Emails the invoice to the customer

Step 1: Scaffold with --template document

The document template pre-configures the output structure and Document/Render@1 step so you do not have to wire them manually.

bash
1npx cxtms create workflow generate-invoice --template document

Read the generated file to see the scaffold structure, then open it for editing. The key sections to update are inputs, the Document/Render@1 step's template and data, and the final Email/Send step you will add.


Step 2: Define the Input

Replace the placeholder inputs with a single orderId input that the workflow caller provides:

yaml
1inputs:
2 - name: orderId
3 type: text
4 props:
5 displayName: "Order ID"
6 description: "The ID of the order to invoice"
7 required: true

This is the only external input needed — all other data comes from querying the order.


Step 3: Query the Order with Charges

Add a Query/GraphQL step in the main activity to fetch the order, its customer details, and its charges. Use null-safe paths throughout:

yaml
1- task: "Query/GraphQL@1"
2 name: GetOrder
3 inputs:
4 query: |
5 query GetOrderForInvoice($orderId: String!) {
6 order(id: $orderId) {
7 orderId
8 orderNumber
9 invoiceDate
10 customer {
11 name
12 email
13 address {
14 street
15 city
16 country
17 }
18 }
19 charges {
20 description
21 quantity
22 unitPrice
23 amount
24 }
25 totalCharges
26 }
27 }
28 variables:
29 orderId: "{{ inputs.orderId }}"

After this step, the order data is available as Main?.GetOrder?.order?.


Step 4: Render the PDF Invoice

Add a Document/Render@1 step with an HTML invoice template. The data map binds query outputs to template variables:

yaml
1- task: "Document/Render@1"
2 name: RenderInvoice
3 inputs:
4 template:
5 engine: "handlebars"
6 recipe: "chrome-pdf"
7 content: |
8 <html>
9 <head>
10 <style>
11 body { font-family: Arial, sans-serif; margin: 48px; color: #222; }
12 h1 { margin-bottom: 4px; }
13 .meta { color: #555; margin-bottom: 24px; }
14 table { width: 100%; border-collapse: collapse; margin-top: 16px; }
15 th { background: #f0f0f0; text-align: left; padding: 8px 12px;
16 border-bottom: 2px solid #ccc; }
17 td { padding: 8px 12px; border-bottom: 1px solid #e0e0e0; }
18 .total-row td { font-weight: bold; border-top: 2px solid #ccc;
19 border-bottom: none; }
20 </style>
21 </head>
22 <body>
23 <h1>Invoice #{{orderNumber}}</h1>
24 <div class="meta">
25 <div>Date: {{invoiceDate}}</div>
26 <div>Bill To: {{customerName}}</div>
27 <div>{{customerStreet}}, {{customerCity}}, {{customerCountry}}</div>
28 </div>
29 <table>
30 <tr>
31 <th>Description</th>
32 <th>Qty</th>
33 <th>Unit Price</th>
34 <th>Amount</th>
35 </tr>
36 {{#each charges}}
37 <tr>
38 <td>{{description}}</td>
39 <td>{{quantity}}</td>
40 <td>{{unitPrice}}</td>
41 <td>{{amount}}</td>
42 </tr>
43 {{/each}}
44 <tr class="total-row">
45 <td colspan="3">Total</td>
46 <td>{{totalCharges}}</td>
47 </tr>
48 </table>
49 </body>
50 </html>
51 data:
52 orderNumber: "{{ Main?.GetOrder?.order?.orderNumber? }}"
53 invoiceDate: "{{ Main?.GetOrder?.order?.invoiceDate? }}"
54 customerName: "{{ Main?.GetOrder?.order?.customer?.name? }}"
55 customerStreet: "{{ Main?.GetOrder?.order?.customer?.address?.street? }}"
56 customerCity: "{{ Main?.GetOrder?.order?.customer?.address?.city? }}"
57 customerCountry: "{{ Main?.GetOrder?.order?.customer?.address?.country? }}"
58 charges: "{{ Main?.GetOrder?.order?.charges? }}"
59 totalCharges: "{{ Main?.GetOrder?.order?.totalCharges? }}"
60 outputs:
61 - name: document
62 mapping: "document"

The rendered PDF bytes are now available as Main?.RenderInvoice?.document?.


Step 5: Create an Attachment on the Order

Store the rendered PDF on the order entity so it is accessible from the UI:

yaml
1- task: "Attachment/Create"
2 name: StoreInvoice
3 inputs:
4 entityName: "Order"
5 entityId: "{{ inputs.orderId }}"
6 fileName: "invoice-{{ Main?.GetOrder?.order?.orderNumber? }}.pdf"
7 content: "{{ Main?.RenderInvoice?.document? }}"
8 contentType: "application/pdf"

The fileName includes the order number for easy identification in the attachments list. The content is the same document bytes used for the email in the next step — no re-rendering needed.


Step 6: Email the Invoice to the Customer

Add the final Email/Send step using the stored document bytes:

yaml
1- task: "Email/Send"
2 name: SendInvoice
3 inputs:
4 to: "{{ Main?.GetOrder?.order?.customer?.email? }}"
5 subject: "Invoice for Order {{ Main?.GetOrder?.order?.orderNumber? }}"
6 body: |
7 <p>Dear {{ Main?.GetOrder?.order?.customer?.name? }},</p>
8 <p>Please find your invoice attached for order <strong>{{ Main?.GetOrder?.order?.orderNumber? }}</strong>.</p>
9 <p>If you have any questions, please contact our team.</p>
10 <p>Thank you for your business.</p>
11 attachments:
12 - fileName: "invoice-{{ Main?.GetOrder?.order?.orderNumber? }}.pdf"
13 content: "{{ Main?.RenderInvoice?.document? }}"
14 contentType: "application/pdf"

The same rendered bytes from RenderInvoice?.document? go to both Attachment/Create and Email/Send. The document is rendered only once.


Step 7: Validate

After saving all changes, validate the workflow YAML:

bash
1npx cxtms generate-invoice.yaml

Fix any validation errors, then test by executing the workflow against a known order ID:

bash
1npx cxtms workflow execute generate-invoice.yaml --vars '{"orderId": "your-order-id"}'

Check the execution log to verify the query returned data, the PDF was rendered, the attachment was created, and the email was sent.


Extending the Pattern

Once the basic invoice workflow is working, common extensions include:

  • PDF merging: Add a PdfDocument/Merge@1 step to combine the invoice with a packing list or customs document before emailing.
  • Conditional sending: Add a condition on the SendInvoice step so it only runs when the customer has an email address on file.
  • Template emails: Replace the inline body with a template + templateData for branded HTML email layouts managed outside the workflow YAML.
  • Entity triggers: Change from a Manual trigger to an Order entity trigger so the invoice generates automatically when an order reaches a specific status.

Summary

In this lesson you built a complete invoice document workflow by following these steps:

  1. Scaffold with --template document to get the correct output structure
  2. Define orderId as the required input
  3. Query the order with customer details and charges using Query/GraphQL@1
  4. Render a PDF invoice using Document/Render@1 with the Handlebars engine and chrome-pdf recipe
  5. Store the PDF on the order entity using Attachment/Create
  6. Email the invoice to the customer using Email/Send with the rendered bytes as an attachment
  7. Validate with npx cxtms and test with workflow execute
Invoice Document Workflow - Anko Academy