Aptiwise
Aptiwise
Aptiwise DocumentationForm Layout Feature - Implementation CompleteForm Layout Feature Implementation Guide
Getting Started
ApprovalML YAML Syntax ReferenceApproval Types and Step TypesForm Field Types ReferenceForm LayoutsAI-Generated Layout & Styling YAMLMulti-Page Form YAML Enhancement
User Guide
Workflow Types & Patterns
Markup language

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:

StyleAppearanceUse Case
warningAmber/Yellow backgroundChanges requiring attention, diff results
dangerRed backgroundCritical items, deletions, errors
successGreen backgroundPositive outcomes, confirmations
infoBlue backgroundInformational, read-only context
- name: "change_summary"
  type: "textarea"
  label: "Change Summary"
  style: "warning"           # Renders with amber background to draw attention
  required: false

Example: 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: 50000

Advanced 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 to true to allow multiple captures, or false (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 directly

Advanced 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 value

Workflow 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: true

Example: 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: true

2. 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: true

3. 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_approval

Multi-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 dict

Then 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_account

join field reference:

FieldRequiredDefaultDescription
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
onNo"id"Key field in join records to match against
pickNo"name"String (single field) or dict {output: source} (multiple fields)
paramNo"ids"Parameter name sent to the join source for the collected IDs
separatorNo", "Separator when field is an array and output is a string
as_arrayNofalsetrue → 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.
  • pick as 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%"] (with as_array: true).

Field Mapping

Extract and transform data from API responses into form fields using JSONPath and JSONata.

Three Types of Field Mapping:

  1. 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]"
  1. 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"
  1. 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 via value
  • 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 normally
  • resource (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: end

5. 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: initial

Supported Locations:

  • notify_requestor - Messages sent to workflow requestor
  • notification.message.body - Email body content
  • notification.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

Installation Guide

Previous Page

Approval Types and Step Types

Next Page

On this page

ApprovalML YAML Syntax ReferenceOverviewCore StructureForm FieldsBasic Field TypesField PropertiesField Style PropertyExample: Currency FieldAdvanced Field PropertiesButton-style ChoicesCamera-Only File UploadAdvanced Field Type: Line ItemsWorkflow Steps1. Decision Step (decision)Example: Simple Approve/RejectExample: Multi-Outcome Decision2. Parallel Approval (parallel_approval)3. Conditional Split (conditional_split)4. Automatic Step (automatic)Data Source Fetch (Read)Inline Join — Resolving Relational ID FieldsField MappingResource Update (Write)Complete Example: Data Change Detection Workflow5. Notification (notification)Template Substitution in NotificationsApproval TypesConditional Expression SyntaxOperatorsExamplesDynamic Role ReferencesSettings