Writing Workflows
This guide covers practical patterns for authoring Moco workflow specs. It assumes you have the environment set up — if not, see Development Setup.
Anatomy of a Workflow Spec
Every workflowspec is a YAML file with four top-level sections:
wfspec_name: my-workflow # unique identifier
wfspec_version: 1.0.0 # semantic version
context: # initial variables (optional)
retry_limit: 3
base_url: https://api.example.com
input_data: # runtime inputs (optional)
order_id: # required input (no default)
timeout: 60 # optional input with default
output_name: result # variable to return as the workflow result
body: # workflow logic — a single statement
sequence:
elements:
- transform:
output_data:
- result: "Hello from {{ order_id }}"
body takes exactly one statement. For multi-step workflows use sequence as the root.
Structuring Multi-Step Workflows
Sequential Steps
Wrap steps in a sequence to run them one after another:
body:
sequence:
elements:
- transform:
output_data:
- status: "starting"
- activity:
type: builtin.http_request
input_data:
method: GET
url: "{{ base_url }}/orders/{{ order_id }}"
output_name: order
- transform:
output_data:
- result: "{{ order.total }}"
Parallel Steps
Use parallel to run independent steps concurrently:
- parallel:
join_type: and
elements:
- activity:
name: fetch-user
type: builtin.http_request
input_data:
url: "{{ base_url }}/users/{{ user_id }}"
output_name: user
- activity:
name: fetch-inventory
type: builtin.http_request
input_data:
url: "{{ base_url }}/inventory/{{ product_id }}"
output_name: inventory
Both user and inventory are available after the parallel block completes.
Iteration
Loop over a list with iteration:
- iteration:
iter_type: sequence
input_data: "{{ order_items }}"
body:
activity:
type: process-item
input_data:
item: "{{ iter_item }}"
order_id: "{{ order_id }}"
output_name: item_result
Use iter_type: parallel to process items concurrently.
Working with Variables
Context vs Input Data
context— initial state for the workflow, set at spec-write timeinput_data— parameters supplied at runtime when starting the workflow
context:
max_retries: 3 # always 3, baked into spec
input_data:
order_id: # caller must supply this
region: us-east # caller may override, defaults to "us-east"
Assigning Variables
Use transform to compute and assign variables:
- transform:
output_data:
- subtotal: "{{ quantity * unit_price }}"
- tax: "{{ subtotal * 0.08 }}"
- total: "{{ subtotal + tax }}"
Assignments within a single output_data list are chained — later entries can reference earlier ones.
Reading Activity Output
Assign an output_name to capture what an activity returns:
- activity:
type: builtin.http_request
input_data:
url: https://api.example.com/data
output_name: api_response
- transform:
output_data:
- items: "{{ api_response['items'] }}"
Conditional Logic
Skip a statement by adding a condition:
- activity:
type: send-email
condition: "{{ order.total > 1000 }}"
input_data:
recipient: "{{ order.email }}"
subject: "Large order confirmation"
For branching, combine condition with abort:
- abort:
condition: "{{ not order.is_valid }}"
type: raise
message: "Order {{ order_id }} failed validation"
# This step only runs if the abort didn't fire
- activity:
type: process-payment
input_data:
order: "{{ order }}"
Calling HTTP APIs
The builtin.http_request activity handles most API integrations:
- activity:
type: builtin.http_request
config_data:
base_url: https://api.example.com
headers:
Content-Type: application/json
Authorization: "Bearer {{ api_token }}"
input_data:
method: POST
url: "{{ base_url }}/orders"
body:
order_id: "{{ order_id }}"
items: "{{ cart_items }}"
output_name: create_response
timeout_sec: 30
max_retry_attempts: 3
Use config_data for values that are the same every time (headers, base URL) and input_data for values that change per execution.
Reusing Logic with Child Workflows
Extract reusable logic into a separate workflowspec and call it as a child:
- workflow:
wfspec:
name: calculate-shipping
version: 1.0.0
child_mode: sync
input_data:
weight: "{{ order.weight_kg }}"
destination: "{{ order.ship_to }}"
output_name: shipping_cost
Use child_mode: inline when the child needs access to the parent's full context, and sync when it should run independently with its own isolated context.
Error Handling Patterns
Validate Early
Check preconditions at the top of your workflow before doing expensive work:
body:
sequence:
elements:
- abort:
condition: "{{ not order_id }}"
type: raise
message: "order_id is required"
- abort:
condition: "{{ quantity <= 0 }}"
type: raise
message: "quantity must be positive"
# ... rest of workflow
Retry with Timeouts
Activities retry automatically on failure. Override defaults per activity:
- activity:
type: builtin.http_request
input_data:
url: https://flaky-api.example.com/data
output_name: result
timeout_sec: 10
max_retry_attempts: 5
Cache Expensive Calls
Avoid redundant work by caching activity results:
- activity:
type: builtin.http_request
input_data:
url: https://api.example.com/reference-data
output_name: ref_data
enable_cache: true
cache_policy:
ttl_sec: 3600
A Complete Example
Order processing workflow with validation, payment, and notification:
wfspec_name: process-order
wfspec_version: 1.0.0
input_data:
order_id:
customer_email:
output_name: order_status
body:
sequence:
elements:
# Validate
- abort:
condition: "{{ not order_id }}"
type: raise
message: "order_id required"
# Fetch order
- activity:
type: builtin.http_request
input_data:
method: GET
url: https://api.example.com/orders/{{ order_id }}
output_name: order
timeout_sec: 10
# Process payment and notify in parallel
- parallel:
join_type: and
elements:
- activity:
name: charge-payment
type: builtin.http_request
input_data:
method: POST
url: https://payments.example.com/charge
body:
amount: "{{ order.total }}"
customer_id: "{{ order.customer_id }}"
output_name: payment_result
timeout_sec: 30
- activity:
name: send-confirmation
type: builtin.http_request
input_data:
method: POST
url: https://email.example.com/send
body:
to: "{{ customer_email }}"
subject: "Order {{ order_id }} received"
timeout_sec: 10
- transform:
output_data:
- order_status: "{{ 'paid' if payment_result.success else 'failed' }}"
Next Steps
- Creating Custom Activities — extend Moco with your own activity types
- Testing Workflows — unit and integration testing patterns
- Statements Reference — complete statement syntax
- Workflowspec Reference — expressions, conditions, variable modifiers, and more