These docs cover the S6S workflow engine — the developer playground layer underneath the product. For the main AI Visibility product (brand monitoring, the unified Report, fix actions), start at /docs/ai-visibility.

Expressions

Reference data between workflow steps using double-brace template expressions.

Syntax

Use double curly braces {{expression}} inside any config field value to reference data from previous steps, the trigger, or the current run.

{
  "ref": "format",
  "action": "transform",
  "config": {
    "template": {
      "greeting": "Hello, {{trigger.output.body.name}}!",
      "previous_result": "{{fetch_data.output.body.items[0].title}}"
    }
  }
}

Expressions are resolved at runtime just before the step executes. Every config value that is a string (or a string nested in an object or array) is scanned for double-brace patterns and resolved.

Available Contexts

ExpressionDescription
{{trigger.output.body}}Webhook request body payload (object or string)
{{trigger.output.headers}}Webhook request headers
{{trigger.output.query}}Webhook query string parameters
{{trigger.output.method}}Webhook HTTP method
{{trigger.output.fired_at}}Schedule trigger timestamp (schedule triggers only)
{{steps.<ref>.output.*}}Output from a previous step, accessed by its ref name
{{run.id}}Unique ID of the current run
{{run.startedAt}}ISO timestamp when the run started

Note on step output access: You can reference step outputs as either {{step_ref.output.field}} or {{steps.step_ref.output.field}}. Both forms work. The shorter form ({{step_ref.output.field}}) is more common in recipes.

Output Shapes by Action Type

Each action type produces a different output shape. Knowing the shape tells you which fields to reference in expressions.

ActionOutput Shape
transformThe evaluated template object. Whatever keys you define in template become output fields.
http{ status, statusCode, statusText, body, headers } — HTTP response envelope. API data is inside .body.
codeWhatever the JavaScript expression returns. Objects are returned as-is; primitives are wrapped in { value: ... }.
provider.actionSame as http — compiled to HTTP at deploy time. { status, statusCode, statusText, body, headers }. Always access via .body.
slack{ ok, ts }
telegram{ messageId, chatId, action }
discord{ messageId, channelId }
ai{ text, json?, model, promptTokens, completionTokens }json is populated when responseFormat is "json".
condition{ conditionResult, resolvedValue, operator, compareValue }
email{ messageId, accepted }
gmail{ messages, resultCount } (search), { message } (read), { messageId, threadId } (send)
google_sheets{ rowCount, values } (read) or { updatedRows } (write)
rss{ feedTitle, itemCount, items }
filter{ result }
loop{ items, count }
database{ rows, rowCount }
execute_workflow{ runId, status, workflowSlug, result }

Nested Access

Use dot notation for nested objects and bracket notation for arrays:

ExpressionWhat it accesses
{{trigger.output.body.user.email}}Nested object field from webhook body
{{fetch.output.body.items[0].name}}First element of an array, then a field on that element
{{fetch.output.body.data[2].tags[0]}}Deeply nested array access
{{process.output.results}}Entire array or object (passed as-is)

If a path does not exist at runtime, the expression resolves to undefined and the resulting string contains the literal text undefined.

Common Patterns

Pass webhook data to the next step

{
  "ref": "extract",
  "action": "transform",
  "config": {
    "template": {
      "name": "{{trigger.output.body.name}}",
      "email": "{{trigger.output.body.email}}"
    }
  }
}

Chain API responses

// Step 1: Fetch user data
{
  "ref": "get_user",
  "action": "http",
  "config": { "url": "https://api.example.com/users/123", "method": "GET" }
}

// Step 2: Use the response in another call
{
  "ref": "get_orders",
  "action": "http",
  "config": {
    "url": "https://api.example.com/orders?user={{get_user.output.body.id}}",
    "method": "GET"
  }
}

Conditional execution with run_if

{
  "ref": "premium_notify",
  "action": "slack",
  "config": {
    "channel": "#vip",
    "textTemplate": "VIP order from {{get_user.output.body.name}}"
  },
  "credential": "slack",
  "run_if": "{{get_user.output.body.tier}} == premium"
}

String interpolation in templates

{
  "ref": "compose",
  "action": "transform",
  "config": {
    "template": {
      "subject": "Order #{{trigger.output.body.order_id}} confirmed",
      "body": "Hi {{trigger.output.body.customer_name}},\n\nYour order of {{trigger.output.body.items_count}} items totaling ${{trigger.output.body.total}} has been confirmed.\n\nTrack at: https://example.com/track/{{trigger.output.body.tracking_id}}"
    }
  }
}

AI step with previous output

{
  "ref": "summarize",
  "action": "ai",
  "config": {
    "prompt": "Summarize this article in 3 bullet points:\n\n{{fetch_article.output.body}}",
    "responseFormat": "text"
  }
}

// Access the AI output in the next step:
// {{summarize.output.text}} — the text response
// {{summarize.output.json.key}} — if responseFormat was "json"

Provider.action Output

Critical: Dynamic integration steps (provider.action) and http steps wrap the API response in an envelope. You must access data through .body.

The runtime output of any provider.action or http step is always:

{
  "status": 200,
  "statusCode": 200,
  "statusText": "OK",
  "body": { ... actual API response ... },
  "headers": { ... }
}

This means you must use .body. to reach the API data:

CorrectIncorrect
{{fetch_news.output.body.articles}}{{fetch_news.output.articles}}
{{call_api.output.body.data[0].id}}{{call_api.output.data[0].id}}
{{call_api.output.status}}(status is at the top level, not inside body)

This applies to all provider.action steps (e.g. newsapi.get_top_headlines, pinterest.create_pin) and all http steps. Built-in actions like telegram, slack, code, and transform have their own output shapes and do not use the envelope.

Expressions in Code Steps

Inside code steps, you access data differently. Instead of double-brace templates, you work with native JavaScript objects:

VariableDescription
stepsJavaScript object with all previous step outputs keyed by ref. E.g. steps.fetch_data.body.items
triggerJavaScript object with trigger output. E.g. trigger.body.user_id (webhook)
inputThe step's own resolved config object
Code step example
{
  "ref": "process",
  "action": "code",
  "config": {
    "expression": "const items = steps.fetch_data.body.results; const userId = trigger.body.user_id; return { count: items.length, userId, timestamp: new Date().toISOString() };"
  }
}

Unlike {{expression}} templates, the steps and trigger variables in code steps are real JavaScript objects — you can iterate, destructure, and call methods on them directly.

Learn the Recipe Format

See how expressions fit into the full workflow definition.