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:
- Accepts an
orderIdas input - Queries the order with its charges and customer details
- Renders a PDF invoice using Handlebars and the
chrome-pdfrecipe - Creates an attachment on the order entity
- 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.
bash1npx 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:
yaml1inputs:2 - name: orderId3 type: text4 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:
yaml1- task: "Query/GraphQL@1"2 name: GetOrder3 inputs:4 query: |5 query GetOrderForInvoice($orderId: String!) {6 order(id: $orderId) {7 orderId8 orderNumber9 invoiceDate10 customer {11 name12 email13 address {14 street15 city16 country17 }18 }19 charges {20 description21 quantity22 unitPrice23 amount24 }25 totalCharges26 }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:
yaml1- task: "Document/Render@1"2 name: RenderInvoice3 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: document62 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:
yaml1- task: "Attachment/Create"2 name: StoreInvoice3 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:
yaml1- task: "Email/Send"2 name: SendInvoice3 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:
bash1npx cxtms generate-invoice.yaml
Fix any validation errors, then test by executing the workflow against a known order ID:
bash1npx 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@1step to combine the invoice with a packing list or customs document before emailing. - Conditional sending: Add a condition on the
SendInvoicestep so it only runs when the customer has an email address on file. - Template emails: Replace the inline
bodywith atemplate+templateDatafor 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:
- Scaffold with
--template documentto get the correct output structure - Define
orderIdas the required input - Query the order with customer details and charges using
Query/GraphQL@1 - Render a PDF invoice using
Document/Render@1with the Handlebars engine andchrome-pdfrecipe - Store the PDF on the order entity using
Attachment/Create - Email the invoice to the customer using
Email/Sendwith the rendered bytes as an attachment - Validate with
npx cxtmsand test withworkflow execute