Skip to content

Workflow Reference

This document describes the top-level fields and step configuration in a Mantle workflow YAML file. For connector-specific parameters, see Connectors. For AI tool use, see Tool Use. For a hands-on introduction, start with the Getting Started guide.

Complete Example

name: fetch-and-summarize
description: Fetch data from an API and summarize it with an LLM
max_parallel_executions: 3
on_limit: queue

inputs:
  url:
    type: string
    description: URL to fetch
  max_retries:
    type: number
    description: Maximum number of retries for the HTTP request

triggers:
  - type: cron
    schedule: "0 * * * *"
  - type: webhook
    path: "/hooks/fetch-and-summarize"

hooks:
  on_success:
    - name: notify-success
      action: slack/send
      credential: slack-token
      params:
        channel: "#ops"
        text: "Workflow {{ execution.workflow_name }} completed successfully"
  on_failure:
    - name: alert-failure
      action: slack/send
      credential: slack-token
      params:
        channel: "#ops-alerts"
        text: "Workflow {{ execution.workflow_name }} failed: {{ execution.error }}"

steps:
  - name: fetch-data
    action: http/request
    timeout: 30s
    max_parallel: 5
    retry:
      max_attempts: 3
      backoff: exponential
    params:
      method: GET
      url: "{{ inputs.url }}"

  - name: summarize
    action: ai/completion
    timeout: 60s
    params:
      provider: openai
      model: gpt-4o
      prompt: "Summarize this data: {{ steps.fetch-data.output.body }}"

  - name: post-result
    action: http/request
    if: "steps.summarize.output.key_points.size() > 0"
    params:
      method: POST
      url: https://hooks.example.com/results
      body:
        summary: "{{ steps.summarize.output.summary }}"

Top-Level Fields

FieldTypeRequiredDescription
namestringYesUnique identifier for the workflow. Must be kebab-case: lowercase letters, digits, and hyphens. Pattern: ^[a-z][a-z0-9-]*$.
descriptionstringNoHuman-readable description of what the workflow does.
inputsmapNoInput parameters the workflow accepts at runtime.
triggerslistNoAutomatic triggers that start the workflow. See Triggers.
max_parallel_executionsintegerNoMaximum number of concurrent executions of this workflow. When the limit is reached, behavior is controlled by on_limit. Default: unlimited.
on_limitstringNoWhat to do when max_parallel_executions is reached. One of: queue (wait until a slot opens), reject (fail immediately). Default: queue.
hooksobjectNoLifecycle hooks that run after the workflow completes. See Hooks.
stepslistYesOrdered list of steps to execute. At least one step is required.

Name Rules

The workflow name is the primary identifier used across validate, apply, plan, and run. It must:

  • Start with a lowercase letter
  • Contain only lowercase letters (a-z), digits (0-9), and hyphens (-)
  • Not start or end with a hyphen

Valid examples: fetch-data, my-workflow-v2, a1

Invalid examples: Fetch-Data, fetch_data, -fetch, 123abc

Inputs

Inputs define the parameters a workflow accepts when triggered. Each input is a key-value pair in the inputs map.

inputs:
  url:
    type: string
    description: URL to fetch
  verbose:
    type: boolean
    description: Enable verbose output
  max_items:
    type: number
    description: Maximum number of items to process

Input Fields

FieldTypeRequiredDescription
(key)stringYesInput parameter name. Must be snake_case: lowercase letters, digits, and underscores. Pattern: ^[a-z][a-z0-9_]*$.
typestringNoData type. One of: string, number, boolean.
descriptionstringNoHuman-readable description.

Input Name Rules

Input names use snake_case (underscores), not kebab-case (hyphens). This is intentional — input names appear in CEL expressions where hyphens would be interpreted as subtraction.

Valid: url, max_retries, api_key

Invalid: URL, max-retries, apiKey, 123abc

Steps

Steps are the building blocks of a workflow. Each step invokes a connector action and can optionally include conditional logic, retry policies, timeouts, and explicit dependencies. Steps without dependencies run concurrently; use depends_on to declare explicit ordering. See Parallel Execution.

steps:
  - name: fetch-data
    action: http/request
    timeout: 30s
    retry:
      max_attempts: 3
      backoff: exponential
    if: "inputs.url != ''"
    params:
      method: GET
      url: "{{ inputs.url }}"

Step Fields

FieldTypeRequiredDescription
namestringYesUnique name within the workflow. Must be kebab-case: ^[a-z][a-z0-9-]*$.
actionstringYesConnector action to invoke, in connector/action format.
paramsmapNoParameters passed to the connector action. Structure depends on the action.
ifstringNoCEL expression. The step runs only if this evaluates to true.
retryobjectNoRetry policy for this step. See Retry Policy.
timeoutstringNoMaximum duration for the step. Uses Go duration format (e.g., 30s, 5m, 1h).
credentialstringNoName of a stored credential to inject into this step. See Secrets Guide.
depends_onlist of stringsNoDeclares explicit dependencies on other steps for parallel execution. See Parallel Execution.
max_parallelintegerNoMaximum number of concurrent instances of this step (useful when the step is generated dynamically from a fan-out). Default: unlimited.
continue_on_errorbooleanNoWhen true, workflow execution continues even if this step fails after exhausting retries. Default is false. See Error Handling.

Step Name Rules

Step names follow the same rules as the workflow name: kebab-case, starting with a lowercase letter. Step names must be unique within a workflow — duplicate names cause a validation error.

Step names matter because you reference step outputs in CEL expressions using steps.STEP_NAME.output.

Note on hyphenated step names in CEL: When a step name contains hyphens (e.g., fetch-data), you can use dot notation in template strings ({{ steps.fetch-data.output.body }}), but in if expressions you must use bracket notation: steps['fetch-data'].output.body. This is because CEL interprets hyphens as subtraction in expression context.

Parallel Execution

By default, Mantle builds a directed acyclic graph (DAG) from your steps and runs steps concurrently when their dependencies allow it. You control ordering with depends_on and through implicit dependencies detected from CEL expressions.

How dependencies are resolved:

  • Explicit dependencies — list step names in depends_on to declare that a step must wait for those steps to complete before it can start.
  • Implicit dependencies — Mantle analyzes CEL expressions in params and if fields. If a step references steps.fetch-data.output, the engine automatically adds fetch-data as a dependency. You do not need to list implicit dependencies in depends_on.
  • Skipped steps count as resolved — if a step is skipped (its if condition evaluated to false), downstream steps that depend on it are unblocked and can proceed.

Fan-out/fan-in example:

name: fan-out-fan-in
description: Run two API calls in parallel, then merge results

steps:
  - name: fetch-users
    action: http/request
    params:
      method: GET
      url: https://api.example.com/users

  - name: fetch-orders
    action: http/request
    params:
      method: GET
      url: https://api.example.com/orders

  - name: merge-results
    action: ai/completion
    credential: openai
    depends_on:
      - fetch-users
      - fetch-orders
    params:
      model: gpt-4o
      prompt: >
        Correlate these users and orders:
        Users: {{ steps['fetch-users'].output.body }}
        Orders: {{ steps['fetch-orders'].output.body }}

In this workflow, fetch-users and fetch-orders have no dependencies on each other, so they run concurrently. The merge-results step declares both as explicit dependencies via depends_on and waits for both to complete before it starts.

Retry Policy

The retry policy controls what happens when a step fails.

retry:
  max_attempts: 3
  backoff: exponential
FieldTypeRequiredDescription
max_attemptsintegerYesMaximum number of attempts. Must be greater than 0.
backoffstringNoBackoff strategy between retries. One of: fixed, exponential.

If backoff is omitted and retry is present, the default behavior depends on the engine implementation.

Timeout

The timeout field accepts Go duration strings. These consist of a number followed by a unit suffix:

UnitSuffixExample
Millisecondsms500ms
Secondss30s
Minutesm5m
Hoursh1h

You can combine units: 1m30s means one minute and thirty seconds.

The timeout must be a positive duration. 0s and negative values are invalid.

Error Handling

By default, if a step fails after exhausting its retry policy, the workflow stops and the entire execution is marked as failed. The continue_on_error field changes this behavior.

continue_on_error

When continue_on_error: true, the step’s failure does not stop the workflow. Instead, the error is captured and made available to downstream steps via steps['step-name'].error. This allows workflows to implement custom error handling, logging, or recovery logic.

steps:
  - name: backup
    action: s3/put
    continue_on_error: true
    timeout: "5m"
    params:
      bucket: my-backups
      key: "data.csv"
      content: "{{ steps.fetch.output.body }}"

  - name: notify-on-failure
    action: slack/send
    credential: slack-token
    if: "steps['backup'].error != null"
    params:
      channel: "#ops-alerts"
      text: "Backup failed: {{ steps['backup'].error }}"

Available Error Fields

  • steps['name'].errornull for successful or skipped steps; a string error message for failed steps. The field is always present in the CEL context, but is only practically reachable on a step that has continue_on_error: true — without that flag, a step failure halts the workflow before any downstream step can inspect the error.

:::caution[Error String Contents] Error strings in steps['name'].error may contain internal infrastructure details such as database hostnames, S3 bucket names, Docker image paths, or IMAP server banners. Avoid forwarding raw error strings to external services (Slack, email, webhooks) without sanitization. If error details must be exposed, use a conditional expression that provides a user-friendly message:

text: "{{ steps.backup.error != null ? 'Backup step failed — check logs for details' : 'Backup succeeded' }}"

:::

  • steps['name'].output — Partial output available from the failed step if the connector provided it. Structure depends on the connector.

Example: Fallback Pattern

Use continue_on_error with conditional steps to implement fallback logic:

steps:
  - name: try-primary-api
    action: http/request
    continue_on_error: true
    params:
      method: GET
      url: https://primary-api.example.com/data

  - name: try-backup-api
    action: http/request
    if: "steps['try-primary-api'].error != null"
    params:
      method: GET
      url: https://backup-api.example.com/data

  - name: process-data
    action: http/request
    params:
      method: POST
      url: https://processor.example.com/process
      # Use output from whichever API succeeded
      body: "{{ steps['try-primary-api'].error == null ? steps['try-primary-api'].output.body : steps['try-backup-api'].output.body }}"

Hooks

Lifecycle hooks run after the main workflow steps complete. Use hooks for notifications, cleanup, or post-processing that should happen regardless of whether the workflow succeeded or failed.

hooks:
  on_success:
    - name: notify-success
      action: slack/send
      credential: slack-token
      params:
        channel: "#ops"
        text: "{{ execution.workflow_name }} v{{ execution.workflow_version }} completed"

  on_failure:
    - name: alert-failure
      action: slack/send
      credential: slack-token
      params:
        channel: "#ops-alerts"
        text: "{{ execution.workflow_name }} failed: {{ execution.error }}"

  on_finish:
    - name: record-metrics
      action: http/request
      params:
        method: POST
        url: https://metrics.internal/workflow-complete
        body:
          workflow: "{{ execution.workflow_name }}"
          status: "{{ execution.status }}"
          duration_ms: "{{ execution.duration_ms }}"

Hook Blocks

BlockWhen it runs
on_successOnly when all steps completed successfully.
on_failureOnly when the workflow failed (one or more steps failed without continue_on_error).
on_finishAlways runs after the workflow completes, regardless of success or failure. Runs after on_success or on_failure.

Each block contains a list of steps with the same structure as regular workflow steps (name, action, params, credential, timeout, etc.). Hook steps execute sequentially within their block.

Hook CEL Variables

In addition to the standard CEL variables (inputs, steps, env, trigger), hook steps have access to execution metadata:

VariableTypeDescription
execution.idstringUUID of the current execution.
execution.workflow_namestringName of the workflow.
execution.workflow_versionintegerVersion of the workflow definition.
execution.statusstringFinal execution status: completed, failed, cancelled.
execution.errorstringError message if the workflow failed; empty string otherwise.
execution.duration_msintegerTotal execution duration in milliseconds.
execution.started_atstringISO 8601 timestamp of when the execution started.
hooks['hook-name'].outputmapOutput of a previously executed hook step (within the same block).

Hook Error Behavior

Hook failures do not change the workflow’s final status. If a workflow succeeded but an on_success hook fails, the execution status remains completed. Hook errors are logged and recorded in the audit trail.


CEL Expressions

Mantle uses CEL (Common Expression Language) for conditional logic and data access between steps. See the Expressions guide for practical examples. CEL expressions appear in two places:

  1. if fields — determine whether a step runs
  2. Template strings in params — reference data from inputs and previous steps using {{ expression }} syntax

Available Variables

VariableDescription
inputs.NAMEValue of the input parameter NAME.
steps.STEP_NAME.outputOutput of the step named STEP_NAME. The structure depends on the connector.
env.NAMEValue of the environment variable NAME.
trigger.payloadRequest body from a webhook trigger, parsed as JSON. Only available for webhook-triggered executions.

Expression Examples

Reference an input:

url: "{{ inputs.url }}"

Reference a previous step’s output:

prompt: "Summarize: {{ steps.fetch-data.output.body }}"

Conditional execution based on step output:

if: "steps.summarize.output.key_points.size() > 0"

Boolean logic:

if: "inputs.verbose == true && steps.fetch-data.output.status == 200"

String operations:

if: "steps.fetch-data.output.body.contains('error') == false"

CEL Type Safety

CEL is a strongly typed language. If you compare values of different types, the expression will fail at evaluation time. For example, inputs.count > "5" fails because you are comparing a number to a string.

Triggers

Triggers define how a workflow is started automatically when Mantle runs in server mode (mantle serve). A workflow can have zero, one, or multiple triggers.

triggers:
  - type: cron
    schedule: "*/5 * * * *"
  - type: webhook
    path: "/hooks/my-workflow"

Triggers are optional. Without them, the workflow can still be executed manually with mantle run or via the REST API (POST /api/v1/run/{workflow}).

Trigger Fields

FieldTypeRequiredDescription
typestringYesTrigger type. One of: cron, webhook, email.
schedulestringCron onlyCron expression defining the schedule. Required when type is cron.
pathstringWebhook onlyURL path for the webhook endpoint. Required when type is webhook.
mailboxstringEmail onlyCredential name for the email account (IMAP-compatible). Required when type is email.
folderstringEmail onlyFolder to monitor (e.g., INBOX). Default: INBOX.
filterstringEmail onlyFilter messages: all, unseen, recent, flagged. Default: unseen.
poll_intervalstringEmail onlyHow often to check for new messages (e.g., 30s, 5m). Default: 60s.

Trigger Lifecycle

Triggers are managed through the standard IaC lifecycle. When you run mantle apply:

  • New triggers in the YAML are registered with the server
  • Changed triggers (e.g., updated cron schedule) are updated
  • Removed triggers (deleted from the YAML) are deregistered

You do not manage triggers separately. The workflow definition is the single source of truth.

Validation Rules Summary

Mantle validates the following rules when you run mantle validate or mantle apply:

RuleError Message
Workflow name is requiredname is required
Workflow name must be kebab-casename must match ^[a-z][a-z0-9-]*$
At least one step is requiredat least one step is required
Input names must be snake_caseinput name must match ^[a-z][a-z0-9_]*$
Input types must be validtype must be one of: string, number, boolean
Step names are requiredstep name is required
Step names must be kebab-casestep name must match ^[a-z][a-z0-9-]*$
Step names must be uniqueduplicate step name "NAME"
Step actions are requiredstep action is required
Retry max_attempts must be > 0max_attempts must be greater than 0
Retry backoff must be validbackoff must be one of: fixed, exponential
Timeout must be a valid durationinvalid duration: ...
Timeout must be positivetimeout must be a positive duration
Dependency cycle detectedcycle detected in step dependencies
depends_on references undefined stepreferences undefined step "NAME"

Validation errors include line and column numbers when available, formatted as:

workflow.yaml:3:1: error: step name must match ^[a-z][a-z0-9-]*$ (steps[0].name)

Minimal Valid Workflow

The smallest valid workflow contains a name and one step with an action:

name: hello
steps:
  - name: greet
    action: http/request
    params:
      method: GET
      url: https://httpbin.org/get