Octov0.11.7
ReferenceBlocks

Integration

Call other flows, run local commands, validate JWTs, and render template resources.

flow-ref

Invokes another flow by name. The target must be a flow with no source: (a source-less flow gets an implicit source and becomes callable by name). The called flow receives a fresh sub-message, a clone of the body and variables under a new event ID, so the sub-invocation correlates independently of the caller's own terminal event.

SettingTypeRequiredDefaultDescription
flowstringYesnoneName of the flow to invoke.
oneWayboolNofalseFire-and-forget when true; otherwise wait and fold the result back in.

Synchronously (the default) the caller waits: the called flow's body replaces the caller's body and its variables are merged over the caller's key-by-key. If the called flow drops the message, the caller's message continues unchanged.

With oneWay: true the message is dispatched and the caller continues immediately, ignoring the result. Use it for audit trails and side effects.

Errors: a synchronous call propagates the called flow's error to the caller; a one-way call errors only if dispatch itself fails.

# Fire-and-forget audit; the request keeps moving immediately.
- type: flow-ref
  name: audit-async
  settings:
    flow: audit
    oneWay: true

# Synchronous delegation; enrich-order's body + variables fold back in.
- type: flow-ref
  name: enrich-sync
  settings:
    flow: enrich-order

See Composing flows for patterns.

cli-run

Runs a local program and carries on with how it ended. The events path watches its output a line at a time while the command is still running, which is what makes streaming a long command to a caller possible.

- type: cli-run
  name: build
  program: '"go"'
  args: '["build", "./..."]'
  workDir: /src
  env: [PATH, HOME]
  timeout: 5m
  events:
    process:
      - type: sse-event
        name: line
        settings:
          event: line
          data: '{"stream": body.type, "text": body.text}'

The block is synchronous: it returns once the command has exited. A block that resumed the flow once per line would end the caller's own chain, so an HTTP request would be answered and its connection torn down while the command was still running.

Settings

FieldMeaning
programCEL expression naming the program: a bare name resolved through $PATH, or an absolute path.
argsCEL expression yielding a list of strings, passed as argv.
stdinCEL expression written to the process's standard input, then closed.
allowEvery program this block may run. Empty means no restriction (see below).
allowInterpretersPermit an interpreter on allow. Default false.
envEnvironment variables passed to the child, by name. Nothing else is passed.
workDirDirectory the command runs in.
timeoutHow long it may run before it is killed. Default 30s.
maxOutputBytesCap on captured output when nothing is streaming it. Default 1 MiB.
onExitfail (default) errors the block on a non-zero exit; continue carries on.
eventsObserver sub-flow, run once per event. Its result is discarded.
emitWhich events reach it: stdout, stderr, exit. Empty emits all three.

The allow list

allow is optional. A block without one may run anything that resolves, which keeps cli-run pleasant for local, iterative work.

# Fine: the program is written right there.
program: '"go"'

program is an expression, though, so which program runs can come from the message. That is what lets an ai-agent tool branch hand a model a set of commands and let it choose. There allow is the only thing standing between the caller and arbitrary execution, and it is checked on every message, before anything is spawned:

program: vars.program
allow: [ls, df, date]

A block with no allow whose program comes from the message will run whatever it is handed. That is what you want at a terminal, and it is remote code execution if the flow is reachable by anyone else. Declare an allow list for anything you deploy. The builder logs a warning at startup for that combination: no list and a message-supplied program.

Rules worth knowing:

  • An entry is a bare name resolved through $PATH, or an absolute path used as it stands. Every entry is resolved at build time, so an entry naming a program that is not installed fails at startup rather than on the message that first reached for it.
  • Matching is on the resolved path, not the string you typed. .. segments are normalized, so /bin/../bin/sh cannot dodge an entry for /bin/sh. Symlinks are deliberately not followed: on a busybox system every applet is a symlink to the same binary, so following them would make an allow list of ["cat"] also admit sh. The cost is that on a usr-merged Linux /bin/ls and /usr/bin/ls do not match each other, so write the list the way the flow names the program.
  • Interpreters are refused on an explicit list unless allowInterpreters: true, because listing sh or python allows everything it can reach. The check applies only to a list; a block with no list has already said "anything". It is a guardrail against an easy mistake, not a sandbox: the list can never be complete (git runs a pager, tar has --to-command).

What is not a risk

  • No shell is ever involved. Arguments are passed as argv, so a semicolon, a pipe or a $(…) in an argument is just bytes to the callee.
  • The child inherits nothing. It sees exactly the variables named in env and no others, so a command cannot read an API key the integration holds.

Output

The chain carries on with a body of {exitCode, stdout, stderr, lines}. The output is captured whether or not an events path watched it go by. maxOutputBytes caps what is kept, while lines always counts everything the command produced, so a truncated capture is visible rather than silent.

Each event's body is {type, text, index} for stdout/stderr, and {type: "exit", exitCode, lines} at the end. The exit event fires for every run, even one that printed nothing. It still passes through emit, so a path that wants it must not filter it out.

An events path that stops the flow (an sse-event whose caller hung up, with ifClosed: stop) stops the command too, rather than letting it produce output nobody will read.

Backpressure is free. The events path runs between reads of the process's pipes, so a slow sink blocks the child on write rather than buffering its output without bound.

See samples/cli-run.yaml, samples/cli-sse.yaml and samples/cli-agent.yaml.

jwt-validate

A filter block that authenticates HTTP requests: it verifies a bearer JWT against an OIDC provider and, on failure, stops the flow with a configurable response (401 by default) so the rest of the chain never runs. On success it injects the verified claims into a variable (default vars.jwt) and the token subject into vars.sub.

The token is read from a request-header variable (default Authorization, with a Bearer prefix stripped case-insensitively). The HTTP route source must copy that header into vars: set headers: [Authorization] on the source.

SettingTypeRequiredDefaultDescription
modeenum discover | jwks | inlineNodiscoverHow signing keys are resolved: OIDC discovery from issuer, direct JWKS fetch from jwksUrl, or an inline public key.
issuerstringFor discovernoneExpected token issuer (iss). In discover mode it is also the discovery URL (<issuer>/.well-known/openid-configuration). For jwks/inline the iss check is applied when set and skipped when empty.
audiencestringNononeExpected token audience (aud). Empty skips the audience check.
algorithmslist of stringsNogo-oidc defaultsAccepted signing algorithms, e.g. [RS256].
jwksUrlstringFor jwksnoneJWKS endpoint fetched directly (cached and refreshed).
publicKeystringFor inline*noneLiteral PEM public key or certificate (RSA/ECDSA, PKIX or a certificate).
publicKeyResourcestringFor inline*noneResource id whose content is the PEM public key. *Set exactly one of publicKey / publicKeyResource.
tokenHeaderstringNoAuthorizationVariable holding the bearer token (a Bearer prefix is stripped).
claimsVarstringNojwtVariable the verified claims map is stored under on success (the subject is also set on vars.sub).
rejectStatusintNo401HTTP status of the built-in rejection response.
resourceMetadataUrlstringNononeProtected-resource-metadata URL (RFC 9728) advertised as resource_metadata="…" in the WWW-Authenticate challenge; set it to make the endpoint an MCP-compliant protected resource.
disableWwwAuthenticateboolNofalseBy default, when an issuer is configured, a rejection sets a WWW-Authenticate: Bearer challenge in vars (list WWW-Authenticate in the route source's responseHeaders to emit it). Set true to opt out.

Behavior notes:

  • Key material for discover/jwks is resolved lazily on the first request, so a briefly unreachable provider does not fail deployment; inline needs no network and fails fast on a bad key.
  • A rejection sets the body to {"error": "unauthorized"}, sets vars.httpStatus to rejectStatus, and requests the flow stop. A missing token yields a bare challenge; a present-but-invalid token adds error="invalid_token" (per RFC 6750).
  • A key-resolution failure (provider unreachable) is an operational error: it aborts the flow rather than rejecting the request.
flows:
  - name: me
    source:
      connector: api
      type: http
      settings:
        path: /me
        headers: [Authorization]            # forward the token into vars
        responseHeaders: [WWW-Authenticate] # emit the challenge on rejection
    process:
      - type: jwt-validate
        name: require-auth
        settings:
          mode: discover
          issuer: ${OIDC_ISSUER}
          audience: ${OIDC_AUDIENCE}
          algorithms: [RS256]
      # Only reached with a valid token.
      - type: set-payload
        settings:
          value: '{"sub": vars.sub, "email": vars.jwt.email}'

See Validation and auth and MCP auth for full walkthroughs.

template-resource

Renders a declared template resource against the current message. The template is declared under resources.templates in the flow file and embeds {{ CEL }} expressions over body, vars, env, and now. The rendered text replaces the message body by default, or is stored in a variable when target is set.

SettingTypeRequiredDefaultDescription
idstringYesnoneThe template to render: its resources.templates alias (the as), or its resource path when unaliased.
targetstringNononeVariable to store the rendered text in. Empty replaces the message body.
rawBodyboolNofalseWrite the rendered text as a raw-content body {contentType, rawData} so a connector (e.g. the HTTP source) serves it verbatim. Body only: target must be empty.
contentTypestringWhen rawBodynoneMIME type recorded on the raw body, e.g. text/html.

Errors: an unknown template id or a render failure (a failing embedded expression) errors the block. Declared resources are loaded when the config loads, so a missing template file fails at deployment.

resources:
  templates:
    - resource: templates/welcome.tmpl
      as: welcome

flows:
  - name: greet
    process:
      - type: template-resource
        settings:
          id: welcome
          target: rendered
      - type: set-payload
        settings:
          value: '{"viaBlock": vars.rendered, "viaCel": templateResource("welcome")}'

The templateResource(alias) CEL function renders the same template inline in an expression. See Resources for how templates are declared, and Serving HTML for the raw-body pattern.

On this page