Skip to main content

Expression Syntax

Expressions are the heart of Moco's dynamic behavior. They use Python syntax enclosed in {{ }} delimiters to evaluate data at runtime.

Basic Syntax

Expressions use double curly braces:

# String interpolation
greeting: "Hello, {{ name }}!"

# Full evaluation
total: "{{ price * quantity }}"

# Boolean expression
is_valid: "{{ price > 0 and quantity > 0 }}"

# Complex expression
result: "{{ sum([x * 2 for x in range(10)]) }}"

Expression Types

Moco supports different expression evaluation modes.

Python (Default)

Standard Python expression evaluation:

# Arithmetic
result: "{{ 10 + 20 * 3 }}" # 70

# String methods
upper: "{{ name.upper() }}"

# List comprehension
squares: "{{ [x**2 for x in range(5)] }}" # [0, 1, 4, 9, 16]

# Dictionary access
value: "{{ data['key'] }}"
value: "{{ data.get('key', 'default') }}"

# Conditional
discount: "{{ price * 0.1 if price > 100 else 0 }}"

Python Glom

Path-based data extraction using the glom library:

# Extract nested value
user_name: "{{ user.profile.name }}"

# With type specification
user_name#python_glom: "user.profile.name"

# Extract list of values
all_names#python_glom: "users.*.name"

# Complex glom spec
data#python_glom: "orders.0.items.*.price"

Literal

No evaluation - treat as literal string:

# Keep template syntax as-is
template#literal: "{{ this_is_not_evaluated }}"

# Useful for passing templates to other systems
jinja_template#literal: "Hello {{ user_name }}!"

Jinja2

Jinja2 template evaluation:

# Jinja2 template
message#jinja: >
Dear {{ customer.name }},
Your order #{{ order.id }} has been {{ order.status }}.
{% if order.status == 'shipped' %}
Tracking: {{ order.tracking_number }}
{% endif %}

Available in Expressions

Python Builtins

Whitelisted safe builtins (no eval, exec, open, __import__):

abs, all, any, bool, dict, enumerate, filter, float, int, len, list,
map, max, min, range, reversed, round, set, sorted, str, sum, tuple, zip

Examples:

# Built-in functions
total: "{{ sum(prices) }}"
count: "{{ len(items) }}"
maximum: "{{ max(values) }}"
rounded: "{{ round(price, 2) }}"

# Type conversions
as_int: "{{ int(value) }}"
as_str: "{{ str(number) }}"
as_list: "{{ list(range(5)) }}"

Libraries

Pre-imported libraries available in expressions:

import numpy as np
import pandas as pd
import pyarrow as pa
from glom import glom
from bs4 import BeautifulSoup

Examples:

# NumPy
mean: "{{ np.mean([1, 2, 3, 4, 5]) }}"
array: "{{ np.array([1, 2, 3]) }}"

# Pandas
df: "{{ pd.DataFrame({'col1': [1, 2], 'col2': [3, 4]}) }}"
filtered: "{{ df[df['col1'] > 1] }}"

# Glom for complex data extraction
value: "{{ glom(data, 'path.to.nested.value') }}"

Special Variables

VariableDescription
_Root context (all variables)
_raw_outputRaw output from last statement
iter_itemCurrent item in iteration
iter_itemsAll items in iteration
__user_info__User information object
__sys_info__System information

System Info (__sys_info__):

  • trace_id: Root trace identifier (shared across parent and all child workflows)
  • tier: Execution tier
  • workflow_id: Current workflow ID
  • parent_workflow_id: Parent workflow ID (if child)
  • timestamp: Current timestamp

Examples:

# Access all variables
all_data: "{{ _ }}"

# System info
trace: "{{ __sys_info__.trace_id }}"
wf_id: "{{ __sys_info__.workflow_id }}"

# Iteration
- iteration:
input_data: "{{ [1, 2, 3] }}"
body:
transform:
output_data:
- current: "{{ iter_item }}" # 1, then 2, then 3
- all: "{{ iter_items }}" # [1, 2, 3]

Variable Modifiers

Modifiers control how variables are evaluated and stored.

Syntax

variableName[@][#modifier...][#]

Container Scope (@)

Variables with @ are temporary and not saved to global context:

- transform:
output_data:
- temp@: "{{ compute_expensive() }}" # Temporary
- result: "{{ temp * 2 }}" # Can use temp
# temp is NOT saved to context after this

Use for:

  • Intermediate calculations
  • Large data you don't need to persist
  • Reducing context size

Example:

- transform:
output_data:
- subtotal@: "{{ sum(item_prices) }}"
- tax@: "{{ subtotal * 0.08 }}"
- shipping@: "{{ 5.99 if subtotal < 50 else 0 }}"
- total: "{{ subtotal + tax + shipping }}"
# Only 'total' is saved

Debug Logging (#)

Trailing # logs the variable value:

- transform:
output_data:
- debug_value#: "{{ computation() }}" # Logs value
- result: "{{ debug_value * 2 }}"

Force Expression Type

Override default expression evaluation:

- transform:
output_data:
# Force literal (don't evaluate)
- template#literal: "{{ user_name }}"

# Force glom path
- user_name#python_glom: "user.profile.name"

# Force Jinja2
- message#jinja: "Hello {{ name }}!"

# Force python (redundant, but explicit)
- value#python: "{{ 1 + 1 }}"

Multi-stage Evaluation

Chain modifiers for multiple evaluation passes:

- transform:
output_data:
- var1: "result_var"
- var2: "{{ var1 }}" # "result_var"
- result_var: 42
- nested#python#python: "{{ var2 }}" # First: "{{ var1 }}" -> "result_var"
# Second: "{{ result_var }}" -> 42

Use for:

  • Dynamic variable references
  • Meta-evaluation
  • Template-in-template scenarios

Combined Modifiers

Combine multiple modifiers:

# Temporary + debug logging
temp@#: "{{ expensive_calc() }}"

# Container scope + force literal
intermediate@#literal: "{{ template }}"

# Debug + glom + multi-stage
extracted#python_glom#python#: "path.to.value"

Common Patterns

Conditional Logic

# Ternary operator
discount: "{{ price * 0.1 if is_premium else 0 }}"

# Multiple conditions
tier: "{{ 'gold' if points > 1000 else 'silver' if points > 500 else 'bronze' }}"

# Boolean logic
valid: "{{ price > 0 and quantity > 0 and in_stock }}"

List Operations

# List comprehension
doubled: "{{ [x * 2 for x in numbers] }}"

# Filter
positive: "{{ [x for x in numbers if x > 0] }}"

# Map
names: "{{ [user['name'] for user in users] }}"

# Nested
result: "{{ [[x*y for x in row] for row in matrix] }}"

Dictionary Operations

# Dictionary comprehension
squared: "{{ {k: v**2 for k, v in data.items()} }}"

# Filter dictionary
filtered: "{{ {k: v for k, v in data.items() if v > 0} }}"

# Merge dictionaries
merged: "{{ {**dict1, **dict2} }}"

String Operations

# String methods
upper: "{{ text.upper() }}"
lower: "{{ text.lower() }}"
stripped: "{{ text.strip() }}"
replaced: "{{ text.replace('old', 'new') }}"

# String formatting
formatted: "{{ f'Total: ${total:.2f}' }}"

# String interpolation
message: "Hello, {{ name }}! You have {{ count }} messages."

Best Practices

Keep Expressions Simple

# Good: Simple, readable
total: "{{ price * quantity }}"

# Bad: Too complex
result: "{{ sum([item['price'] * item['qty'] * (1 - item.get('discount', 0)) for item in items if item.get('available', True) and item['price'] > 0]) }}"

# Better: Break into steps
- transform:
output_data:
- available_items@: "{{ [i for i in items if i.get('available', True) and i['price'] > 0] }}"
- subtotals@: "{{ [i['price'] * i['qty'] * (1 - i.get('discount', 0)) for i in available_items] }}"
- total: "{{ sum(subtotals) }}"

Use Temporary Variables

# Use @ for intermediate calculations
- transform:
output_data:
- base_price@: "{{ item_price * quantity }}"
- discount@: "{{ base_price * discount_rate }}"
- tax@: "{{ (base_price - discount) * tax_rate }}"
- final_price: "{{ base_price - discount + tax }}"

Debug with #

# Log important values during development
- transform:
output_data:
- input_data#: "{{ input }}"
- processed#: "{{ process(input) }}"
- result: "{{ finalize(processed) }}"

Validate Inputs

# Check for required fields
- abort:
condition: "{{ not email or '@' not in email }}"
type: raise
message: "Invalid email: {{ email }}"

Next Steps