ApprovalML YAML Syntax Reference
ApprovalML YAML Syntax Reference
This page provides a detailed reference for the ApprovalML YAML syntax, designed for AI-powered workflow generation.
Overview
ApprovalML is a YAML-based language for defining business approval workflows. It combines form creation with powerful routing logic.
Core Structure
Every ApprovalML file follows this basic structure:
name: "Workflow Name"
description: "A brief summary of the workflow's purpose."
version: "1.0" # Optional version number
type: "workflow_type" # Optional classification
# Optional: Define who can submit this workflow
submission_criteria:
company_roles: [] # Array of roles that can initiate the workflow
org_hierarchy:
include_paths: ["1.1.*"] # Organizational path patterns for eligibility
# Form definition for data collection
form:
fields: []
# Workflow logic with interconnected steps
workflow:
step_name: {}
# Optional: Advanced settings for the workflow
settings:
timeout: {}
escalation: []
notifications: {}
compliance: {}Form Fields
Define the data to be collected from the user.
Basic Field Types
text: Single-line text input.textarea: Multi-line text area.email: Validated email input.number: Numeric input with validation.currency: Monetary value with currency formatting.date: Date picker.select: Dropdown menu with predefined choices.multiselect: Dropdown for multiple selections.checkbox: A single boolean checkbox.radio: A group of options where only one can be selected.file_upload: For attaching files.
Field Properties
- name: "field_name" # Required: A unique identifier for the field.
type: "field_type" # Required: One of the types listed above.
label: "Display Label" # Required: The text shown to the user.
required: true/false # Required: Specifies if the field must be filled.
placeholder: "Hint text" # Optional: Placeholder text for the input.
accept: ".pdf,.jpg,.png" # For `file_upload`: specifies accepted file types.
multiple: true/false # For `file_upload` or `multiselect`: allows multiple values.
style: "warning" # Optional: Visual emphasis style (see below).
# Validation rules for the field
validation:
min: 0.01 # Minimum value for `number` or `currency`.
max: 10000 # Maximum value for `number` or `currency`.
# Options for `select`, `multiselect`, or `radio`
options:
- value: "option_key"
label: "Display Text"
# Optional currency code for `currency` fields
currency: "USD" # Specifies the ISO currency code (e.g., USD, EUR, JPY).Field Style Property
Use the style property to visually emphasize important fields during approval:
| Style | Appearance | Use Case |
|---|---|---|
warning | Amber/Yellow background | Changes requiring attention, diff results |
danger | Red background | Critical items, deletions, errors |
success | Green background | Positive outcomes, confirmations |
info | Blue background | Informational, read-only context |
- name: "change_summary"
type: "textarea"
label: "Change Summary"
style: "warning" # Renders with amber background to draw attention
required: falseExample: Currency Field
- name: "total_amount"
type: "currency"
label: "Total Amount"
required: true
currency: "USD" # Defaults to the company's primary currency if not set.
validation:
min: 0.01
max: 50000Advanced Field Properties
These properties control the presentation and behavior of certain field types.
Button-style Choices
For radio fields, you can render the options as a button group instead of traditional radio inputs by using the display_as property.
- name: "equipment_check"
type: "radio"
label: "Is all equipment accounted for?"
required: true
display_as: "buttons" # Renders choices as buttons
options:
- { value: "yes", label: "Yes" }
- { value: "no", label: "No" }
- { value: "na", label: "N/A" }Camera-Only File Upload
For file_upload fields, you can force the use of the device camera for capturing images directly.
capture: Set to"environment"to prefer the rear-facing camera or"user"for the front-facing camera.multiple: Set totrueto allow multiple captures, orfalse(default) for a single image.
- name: "site_photo"
type: "file_upload"
label: "Take a photo of the work site"
required: true
accept: "image/*"
multiple: false
capture: "environment" # Opens the rear camera directlyAdvanced Field Type: Line Items
For creating repeatable sections of fields, like items in an invoice.
- name: "items_to_purchase"
type: "line_items"
label: "Items to Purchase"
min_items: 1
max_items: 20
item_fields:
- name: "item_description"
type: "text"
label: "Description"
required: true
- name: "quantity"
type: "number"
label: "Qty"
validation:
min_value: 1
- name: "unit_price"
type: "currency"
label: "Unit Price"
- name: "total"
type: "currency"
label: "Total"
readonly: true
calculated: true
formula: "quantity * unit_price" # Automatically calculates the valueWorkflow Steps
Define the logic and routing of the approval process.
1. Decision Step (decision)
A standard approval step requiring a user to take action. It can have multiple outcomes.
Example: Simple Approve/Reject
manager_approval:
name: "Manager Approval"
type: "decision"
approver: "${requestor.manager}" # Dynamically assigns to the requestor's manager
on_approve:
continue_to: "FinanceReview"
on_reject:
end_workflow: trueExample: Multi-Outcome Decision
Define custom outcomes using on_<action> keys.
triage_step:
name: "Triage Support Ticket"
type: "decision"
approver: "support_lead"
on_technical:
text: "Assign to Technical Team"
continue_to: "TechnicalReview"
on_billing:
text: "Assign to Billing"
continue_to: "BillingReview"
on_close:
text: "Close as Duplicate"
style: "destructive" # Optional UI hint for the action button
end_workflow: true2. Parallel Approval (parallel_approval)
Allows multiple approvers to act simultaneously.
parallel_step:
name: "Parallel Step"
type: "parallel_approval"
description: "Requires input from multiple stakeholders."
approvers:
- role: "purchasing_officer_1"
- role: "purchasing_officer_2"
- role: "purchasing_officer_3"
approval_strategy: "any_one" # Can be `any_one`, `all`, or `majority`
on_approve:
continue_to: "next_step"
on_reject:
end_workflow: true3. Conditional Split (conditional_split)
Routes the workflow dynamically based on form data.
routing_step:
name: "Routing Step"
type: "conditional_split"
description: "Routes based on the request amount."
choices:
- conditions: "amount > 10000 and urgency == 'critical'"
continue_to: "ceo_approval"
- conditions: "department == 'engineering'"
continue_to: "tech_approval"
default:
continue_to: "auto_approve"4. Automatic Step (automatic)
Performs system actions without human intervention. Supports two main operations:
Data Source Fetch (Read)
Fetch data from a configured data source and optionally compare against a baseline resource.
fetch_data:
type: "automatic"
name: "Fetch Current Data"
data_source:
source_id: "src_xxx" # Data source unique ID (connector is implicit)
save_to: "fetched_data" # Save fetched data to this form field
compare_to_resource: "baseline" # Optional: compare with resource baseline
save_diff_to: "diff_result" # Optional: save diff result to this field
field_mapping: # Optional: extract and transform data from the response
customer_name: "$.data.customer.name"
product_name:
source: "$.product.name"
jsonata: "$replace(value, /\\[\\d+\\]\\s*/, '')"
on_complete:
continue_to: "check_changes"Data Source Properties:
source_id: Unique ID of the data source (required)save_to: Form field name to store fetched data (required)compare_to_resource: Resource name to compare against (uses deepdiff)save_diff_to: Form field for diff result ("None"if no changes)join: Inline relational field lookup — resolve integer FK IDs to display values (optional, see below)field_mapping: Extract and transform specific data into form fields (optional)
Inline Join — Resolving Relational ID Fields
ERP systems like Odoo store relational fields as integer IDs or arrays (e.g. tax_ids: [49, 50]).
The join key resolves these to human-readable values in a single step — the engine batch-fetches
the related records, builds an in-memory lookup, and writes the resolved value onto each row
before field_mapping runs. No extra workflow steps, no JSONata, no vars wiring.
Single-field pick — extract one value per join, write to one output field:
fetch_invoice_lines:
type: automatic
data_source:
source_id: src_invoice_lines
save_to: invoice_lines
join:
- field: tax_ids # field on each row (scalar int or array)
source_id: src_tax_api # source to batch-fetch related records from
on: id # key in join records to match against (default: "id")
pick: name # field to extract from each matched record (default: "name")
as: tax_name # output field written onto each row
field_mapping:
invoice_lines:
source: "$.invoice_lines.data"
item_fields:
qty: quantity
unit_price: price_unit
tax: tax_name # already resolved — "Included PPN"
on_complete:
continue_to: manager_approvalMulti-field pick — extract several fields from the same join source in one API call:
data_source:
source_id: src_invoice_lines
save_to: invoice_lines
join:
- field: tax_ids
source_id: src_tax_api
on: id
pick: # dict: output_field_name: source_field_name
tax_name: name # row.tax_name ← record.name (e.g. "Included PPN")
tax_rate: amount # row.tax_rate ← record.amount (e.g. "11%")
tax_account: code # row.tax_account ← record.code (e.g. "4310")
# `as` is not used when pick is a dictThen reference all resolved fields directly in field_mapping:
field_mapping:
invoice_lines:
source: "$.invoice_lines.data"
item_fields:
qty: quantity
unit_price: price_unit
tax: tax_name
rate: tax_rate
account: tax_accountjoin field reference:
| Field | Required | Default | Description |
|---|---|---|---|
field | ✅ Yes | — | Field on each row holding the FK ID(s) |
source_id | ✅ Yes | — | Connector source to batch-fetch related records from |
as | ✅ when pick is a string | — | Output field name written onto each row |
on | No | "id" | Key field in join records to match against |
pick | No | "name" | String (single field) or dict {output: source} (multiple fields) |
param | No | "ids" | Parameter name sent to the join source for the collected IDs |
separator | No | ", " | Separator when field is an array and output is a string |
as_array | No | false | true → output is a list of strings instead of a joined string |
Notes:
- Multiple entries under
join:are supported — each resolves a different FK field. Each entry makes its own batch API call. pickas a dict extracts multiple fields from the same API call — no extra network requests.- For array FKs:
tax_ids: [49, 50]→"Included PPN, GST 10%"(string, default) or["Included PPN", "GST 10%"](withas_array: true).
Field Mapping
Extract and transform data from API responses into form fields using JSONPath and JSONata.
Three Types of Field Mapping:
- Simple JSONPath Extraction
Extract a value from the JSON response using JSONPath syntax:
field_mapping:
customer_name: "$.data.customer.name"
invoice_number: "$.invoice.number"
partner_id: "$.data[0].partner_id[0]"- Nested Array Mapping (Line Items)
Map JSON arrays to line_items fields:
form:
fields:
- name: invoice_lines
type: line_items
label: "Invoice Lines"
item_fields:
- name: product_name
type: text
label: Product
- name: quantity
type: number
label: Qty
- name: price
type: currency
label: Price
workflow:
fetch_invoice:
type: automatic
data_source:
source_id: src_invoice_api
save_to: raw_invoice
field_mapping:
invoice_lines:
source: "$.invoice.invoice_line_ids"
item_fields:
product_name: "display_name"
quantity: "quantity"
price: "price_unit"- JSONata Transformations
Transform data using JSONata expressions for string operations, regex, and more:
field_mapping:
# Remove product ID prefix: [434322544] Plastic Cup -> Plastic Cup
product_name:
source: "$.product.name"
jsonata: "$replace(value, /\\[\\d+\\]\\s*/, '')"
# Concatenate address fields
full_address:
jsonata: "street & ', ' & city & ' ' & zip"
# Extract first 3 characters and uppercase
customer_code:
source: "$.customer.name"
jsonata: "$uppercase($substring(value, 0, 3))"
# Combine first and last name
full_name:
jsonata: "firstName & ' ' & lastName"JSONata Expression Syntax:
- String functions:
$uppercase(),$lowercase(),$substring(),$trim() - Regex replace:
$replace(value, /pattern/, "replacement") - Concatenation: Use
&operator (e.g.,field1 & ' ' & field2) - Context variable: When using
source, access extracted value viavalue - No source: Without
source, the entire payload is available
Common JSONata Examples:
# Remove special characters
clean_text:
source: "$.description"
jsonata: "$replace(value, /[^a-zA-Z0-9\\s]/, '')"
# Format phone number
phone_formatted:
source: "$.phone"
jsonata: "$replace(value, /(\\d{3})(\\d{3})(\\d{4})/, '($1) $2-$3')"
# Conditional value
status_text:
source: "$.status"
jsonata: "status = 'active' ? 'Active' : 'Inactive'"
# Extract domain from email
domain:
source: "$.email"
jsonata: "$substringAfter(value, '@')"Error Handling:
Field mapping is designed to be fault-tolerant:
- If a JSONPath doesn't match, the field is skipped (not populated)
- If an array is empty, it's set as an empty array
[] - Errors are logged for admin diagnosis but workflow continues
- Missing nested fields log warnings but don't crash the workflow
Diff Result Format: The diff result includes markdown-style emphasis for visual highlighting in the approval UI:
⚠️ **3 change(s) detected**
**➕ ADDITIONS:**
• Detected addition at **bindings → item #1 → members**:
Added: **"user:newuser@example.com"**
**➖ REMOVALS:**
• Detected removal at **bindings → item #5 → members**:
Removed: **"user:olduser@example.com"**Text Emphasis Support:
- Text and textarea fields support
**bold**markdown markers - Bold text renders with red color and yellow highlight
- Fields with emphasis get an amber background for attention
Resource Update (Write)
Update a resource with data from a form field.
update_resource:
type: "automatic"
name: "Update Baseline Resource"
resource:
data_from: "fetched_data" # Get data from this form field
resource_name: "baseline" # Update this resource
on_complete:
continue_to: "complete"Resource Properties:
data_from: Form field containing the data to save (required)resource_name: Name of the resource to update (required)
Test Mode Behavior:
data_source(read): Executes normallyresource(write): Logs the action but does NOT modify the resource
Complete Example: Data Change Detection Workflow
name: "Data Compliance Monitor"
type: "compliance"
triggers:
- type: cron
schedule: "*/15 * * * *"
form:
fields:
- name: data_json
type: textarea
label: Current Data
- name: diff_result
type: textarea
label: Change Summary
workflow:
fetch_data:
type: automatic
name: Fetch Current Data
data_source:
source_id: src_xxx
save_to: data_json
compare_to_resource: data-baseline
save_diff_to: diff_result
on_complete:
continue_to: check_changes
check_changes:
type: conditional_split
choices:
- conditions: diff_result != 'None'
continue_to: review
default:
continue_to: no_changes_end
review:
type: decision
name: Review Changes
approver: admin
on_approve:
continue_to: update_baseline
on_reject:
continue_to: rejected_end
update_baseline:
type: automatic
name: Update Baseline
resource:
data_from: data_json
resource_name: data-baseline
on_complete:
continue_to: approved_end
approved_end:
type: end
notify_requestor: Changes approved and baseline updated.
rejected_end:
type: end
notify_requestor: Changes rejected.
no_changes_end:
type: end5. Notification (notification)
Sends a non-blocking notification to specified recipients.
notify_step:
name: "Notify Supervisor"
type: "notification"
description: "Informs the supervisor about the request."
recipients:
- email: "${requestor.supervisor_email}"
notification:
message:
subject: "Request Notification"
body: "A new request has been submitted and requires your attention."
on_complete:
continue_to: "next_step"Template Substitution in Notifications
Use {{ field_name }} syntax to insert form field values into notification messages:
workflow:
manager_approval:
type: decision
approver: manager
on_approve:
notify_requestor: "Invoice {{ invoice_no }} approved on {{ invoice_date }}"
continue_to: next_step
on_reject:
notify_requestor: "Invoice {{ invoice_no }} requires changes"
continue_to: initialSupported Locations:
notify_requestor- Messages sent to workflow requestornotification.message.body- Email body contentnotification.message.subject- Email subject line
Example with Multiple Fields:
on_approve:
notify_requestor: "PO {{ po_number }} for {{ vendor_name }} (Total: ${{ total_amount }}) has been approved"
notification:
message:
subject: "PO {{ po_number }} Approved"
body: |
Purchase Order Details:
- PO Number: {{ po_number }}
- Vendor: {{ vendor_name }}
- Amount: ${{ total_amount }}
- Requested by: {{ requestor_name }}Note: Template substitution is evaluated at runtime with the current form data values.
Approval Types
Define the nature of an approval required from a user.
needs_to_approve(Default): Can approve, reject, or request more information.needs_to_sign: Requires a digital signature.needs_to_recommend: Provides an advisory opinion but cannot block the workflow.needs_to_acknowledge: Requires the user to simply acknowledge receipt.needs_to_call_action: Triggers a system or manual action.receives_a_copy: Receives a notification with no action required.
Conditional Expression Syntax
Used in conditional_split steps to control workflow routing.
Operators
- Comparison:
>,<,>=,<=,==,!= - Logical:
and,or,not - Membership:
in,not in
Examples
# Numeric comparison
conditions: "amount > 1000"
# String equality
conditions: "department == 'engineering'"
# List membership
conditions: "category in ['equipment', 'software']"
# Complex expression
conditions: "(urgency == 'high' or priority >= 4) and amount > 10000"Dynamic Role References
Reference users based on their position in the organizational hierarchy.
${requestor.manager}: The direct manager of the user who submitted the request.${requestor.supervisor}: The supervisor of the requestor.${requestor.department_head}: The head of the requestor's department.
Settings
Configure advanced behavior for the workflow.
settings:
# Step-specific timeouts
timeout:
manager_approval: "72_hours"
ceo_approval: "5_business_days"
# Escalation rules for timeouts
escalation:
- after_timeout: "notify_next_level"
- final_escalation: "ceo"
# Notification preferences
notifications:
send_reminders: true
reminder_intervals: ["24_hours", "2_hours"]
# Compliance requirements
compliance:
receipt_required: true
policy_check: true