Run
Sequential control flow for Telo manifests — Run.Sequence chains invocable steps with if, while, switch, and try/catch blocks.
Why use this
- Manifest-native flow control — branching and looping live in YAML, not in a
JS.Scriptescape hatch. - Typed step results — each step's output is statically typed inside
${{ steps.<name>.result }}, so downstream CEL expressions are validated by the analyzer. - Structured error handling —
try/catchmatches onInvokeErrorcodes; see Structured Errors for the end-to-end flow. - Composes with everything — any
Telo.Invocableresource can be a step, so AI calls, HTTP requests, SQL queries, and your own scripts mix freely.
Kinds
| Kind | Purpose |
|---|---|
Run.Sequence | Run an ordered list of invocable steps with control-flow blocks. |
Run.Value | A pure value/binding invocable — shape a CEL value (or a constant) with no JS.Script. See Run.Value. |
Example
kind: Telo.Application
metadata: { name: pipeline, version: 1.0.0 }
imports:
Run: std/run@latest
---
kind: Run.Sequence
metadata: { name: Pipeline }
steps:
- name: fetch
invoke: { kind: Http.Request, name: GetUser }
- name: greet
invoke: { kind: Console.Print }
inputs:
message: !cel "'Hello, ' + steps.fetch.result.name"
Run.Sequence as an HTTP handler
A Run.Sequence is a Telo.Runnable, so it can be a route handler. The data flow has three seams:
- The route's
inputs:is a CEL map over the request — its result is passed to the handler'sinvoke(). - The sequence's top-level
inputs:declares the input contract (a JSON Schema property map,{}= untyped/dyn). Steps read the values as${{ inputs.<name> }}. - The sequence's
outputs:is a CEL map producing theresult; the route'sreturns:reads it as${{ result }}.
kind: Http.Api
metadata: { name: Api }
routes:
- method: GET
path: /users/:id
inputs:
userId: !cel "request.params.id" # request context → handler invoke()
handler: { kind: Run.Sequence, name: GetUser }
returns:
status: 200
body: !cel "result" # sequence outputs → response
---
kind: Run.Sequence
metadata: { name: GetUser }
inputs:
userId: {} # input contract: untyped (dyn)
steps:
- name: fetch
invoke: { kind: Sql.Query, name: SelectUser }
inputs:
bindings:
- !cel "inputs.userId" # read the declared input
outputs:
user: !cel "steps.fetch.result.rows[0]" # becomes `result` the route sees
inputs: on the sequence (the contract) and inputs: on a step (the values passed to that step's invoke()) are different fields that share a name.
Bringing up dependencies (with: / targets:)
A sequence can stand up its own resources for the duration of its run — a database connection, an Http.Server, a pool — without them being top-level Application resources:
with:declares resources scoped to the sequence. They are initialized before the steps run and torn down when the sequence finishes (or fails).targets:names which of thosewith:resources torun()first (e.g. start a server / run migrations) before the steps execute.
kind: Run.Sequence
metadata: { name: IntegrationCheck }
with:
- kind: Sql.SqliteConnection
metadata: { name: Db }
file: ":memory:"
- kind: Sql.Migrations
metadata: { name: Migrate }
connection: { kind: Sql.SqliteConnection, name: Db }
targets: [ Migrate ] # run() before the steps
steps:
- name: seed
invoke: { kind: Sql.Exec, connection: { kind: Sql.SqliteConnection, name: Db } }
inputs: { sql: !sql "INSERT INTO users (name) VALUES (${{ 'Ada' }})" }
targets: is not Application-only — both Telo.Application and Run.Sequence have it. The difference is lifetime: an Application's targets/resources live for the process; a sequence's with: resources live only for that run. So yes, a Run.Sequence can start an Http.Server (put it in with:, list it in targets:) — useful for self-contained integration tests.
Reference
- Structured Errors — how
try/catchinteracts withInvokeError.