Octov0.4.2
Guides

Transforming Data

Reshape payloads with multi-transform, enrich scopes, and CEL.

In this guide you reshape message data three ways: chain ordered edits with multi-transform, compute in isolation with enrich, and branch and iterate with if, foreach, and switch. It follows samples/multi-transform.yaml, samples/enrich.yaml, and samples/builtins-demo.yaml.

Every expression in this guide is CEL: it sees body (the message payload), vars (the variables), plus eventID and correlationID.

Chain edits with multi-transform

multi-transform applies an ordered list of CEL edits in one block. Each step either replaces the body (setBody) or sets a variable (setVar + value), and the edits are additive: a later step reads what an earlier one produced. A whole chain of set-payload / set-variable blocks collapses into one.

flows:
  - name: order
    process:
      - type: multi-transform
        name: price-order
        settings:
          transforms:
            # 1) compute the subtotal on the body.
            - setBody: '{"orderId": body.orderId, "subtotal": body.qty * body.price}'
            # 2) stash it in a variable (reads the body step 1 produced).
            - setVar: subtotal
              value: body.subtotal
            # 3) add tax to the body (reads the subtotal variable step 2 set).
            - setBody: '{"orderId": body.orderId, "subtotal": vars.subtotal, "total": vars.subtotal * 1.1}'
      - type: log
        name: emit
        settings:
          logger: out
          message: '"order " + body.orderId + " subtotal=" + string(body.subtotal) + " total=" + string(body.total)'

The flow has no source, so you call it directly with octo invoke:

bin/octo invoke --config samples/multi-transform.yaml --flow order \
  --data '{"orderId":"A-1","qty":3,"price":10.0}'

It logs order A-1 subtotal=30 total=33 — step 3 read the variable step 2 set from the body step 1 built.

Compute in isolation with enrich

enrich runs a body sub-flow on an isolated clone of the message, then merges back only what you name: setBody rewrites the original body from the scope's result, setVars pulls named values out. Everything else the scope did — scratch variables, intermediate bodies — stays inside.

Here the scope freely replaces its body with a summary and sets a scratch variable, but only the computed total escapes; setBody is omitted, so the original order body survives untouched:

flows:
  - name: order
    process:
      - type: enrich
        name: derive-total
        # setBody omitted: the incoming order body is preserved.
        setVars:
          total: body.total # pull just the total out of the scope's summary
        body:
          process:
            - type: set-variable
              name: scratch
              settings:
                name: workingNote
                value: '"scope-only, never propagated"'
            - type: set-payload
              name: summary
              settings:
                # The scope rewrites the body on its clone; it stays isolated.
                value: '{"total": body.qty * body.price, "note": "scope-only body"}'
      - type: log
        name: emit
        settings:
          logger: out
          message: '"order " + body.orderId + " total=" + string(vars.total)'
bin/octo invoke --config samples/enrich.yaml --flow order \
  --data '{"orderId":"A-1","qty":3,"price":10.0}'

It logs order A-1 total=30 and returns the order body untouched, with total among the printed message's variables — where enrich put it. Use enrich when a lookup or computation needs elbow room without polluting the message — multi-transform when you want the edits to land directly.

Branch and iterate

The control-flow blocks compose with any transform. samples/builtins-demo.yaml tours them in one flow:

The flow in the visual editor

    process:
      # set-payload: replace the body with an object holding an orders array.
      - type: set-payload
        name: seed-orders
        settings:
          value: '{"firedAt": body.firedAt, "orders": [{"id": 1, "amount": 50}, {"id": 2, "amount": 250}]}'

      # set-variable: stash a threshold the switch below compares against.
      - type: set-variable
        name: set-threshold
        settings:
          name: threshold
          value: "100"

      # if/else: branch on whether there is anything to process.
      - type: if
        name: any-orders
        condition: "size(body.orders) > 0"
        then:
          process:
            - type: log
              settings:
                message: '"processing " + string(size(body.orders)) + " orders at " + body.firedAt'
        else:
          process:
            - type: log
              settings:
                message: '"no orders to process"'

      # foreach: iterate the array, binding each element to `order`.
      - type: foreach
        name: each-order
        items: "body.orders"
        as: order
        body:
          process:
            - type: switch
              name: classify-order
              cases:
                - when: "vars.order.amount >= vars.threshold"
                  process:
                    - type: log
                      settings:
                        message: '"HIGH order " + string(vars.order.id) + " amount=" + string(vars.order.amount)'
              default:
                process:
                  - type: log
                    settings:
                      message: '"low  order " + string(vars.order.id) + " amount=" + string(vars.order.amount)'

      # delete-variable: clean up the scratch variable.
      - type: delete-variable
        name: drop-threshold
        settings:
          name: threshold
  • if takes one condition and a then / else sub-flow.
  • foreach evaluates items to a list and runs its body once per element, binding each to the variable named by as (here vars.order).
  • switch evaluates its when cases in order and runs the first match, or default.

Run it and watch each tick classify the two orders:

bin/octo run --config samples/builtins-demo.yaml

CEL patterns worth stealing

# Default an optional field with a ternary + has():
value: 'has(vars.query.currency) ? vars.query.currency : "USD"'

# CEL is strongly typed: convert before concatenating.
message: '"total=" + string(body.total)'

# Guard a branch on collection size:
condition: 'size(body.orders) > 0'

Where to go next

On this page