Your First Flow
Run a hello-world flow with the octo CLI and invoke a flow by name.
This page runs two flows from the repository's samples/ directory: one on a
schedule, one called from the command line. You need the bin/octo binary from
Installation.
A scheduled flow
samples/hello-world.yaml is the smallest complete config: one connector, one
named processor, and one flow.
service:
name: hello-world
connectors:
- name: ticker
type: cron
processors:
- name: greeter
type: log
settings:
level: info
message: '"hello world! the date is " + body.date'
flows:
- name: greet
source:
connector: ticker
type: cron
settings:
schedule: "0,30 * * * * *"
payload: '{"date": string(now)}'
process:
- ref: greeterconnectors declares a cron connector named ticker. processors defines a
reusable, named block: greeter is a log block whose message is a CEL
expression that can read the message body, vars, eventID, and
correlationID. flows binds them together: the source attaches the flow to
ticker with a six-field cron schedule (with seconds) that fires at seconds 0
and 30 of every minute, the payload expression (with now bound to the fire
time) becomes each message's body, and the one process step references the
named processor with ref: greeter.
Run it:
bin/octo run --config samples/hello-world.yamlrun is the default subcommand, so bin/octo --config samples/hello-world.yaml
works too, and flags accept one or two dashes. The runtime prints a ready
banner, then logs a greeting every 30 seconds:
time=... level=INFO msg="starting runtime" version=0.11.7 connectors=1 flows=1╭────────────────────────────────────────────────╮│ 🐙 octo v0.11.7 is up and ready to roll! 🐙 ││ 1 flow · 1 connector serving │╰────────────────────────────────────────────────╯time=... level=INFO msg="hello world! the date is 2026-07-09T12:00:00Z"time=... level=INFO msg="hello world! the date is 2026-07-09T12:00:30Z"Stop it with Ctrl-C.
Add --watch to reload the runtime whenever the config file changes, so an
edit to the schedule or the message takes effect without a restart.
Invoking a flow directly
A flow with no source: gets an implicit source and becomes callable by name.
samples/hello-invoke.yaml is exactly that:
service:
name: hello-invoke
flows:
- name: greet
process:
- type: set-payload
name: build-greeting
settings:
value: '{"greeting": "hello, " + body.name + "!"}'set-payload replaces the message body with the result of its value
expression. Call the flow with octo invoke, passing the request body as JSON:
bin/octo invoke --config samples/hello-invoke.yaml --flow greet --data '{"name":"Ada"}'{"event_id":"5c12a0e3f47b4d19a6c8f0b21d3e4a97","body":{"greeting":"hello, Ada!"}}What prints is the whole result message: its event_id, the variables
the flow set (none here), and the body. Pipe it through jq '.body' when the
body is all you want. When --data is omitted, invoke reads the body from
stdin:
echo '{"name":"Grace"}' | bin/octo invoke --config samples/hello-invoke.yaml --flow greetIn invoke mode no sources are started, so nothing binds a port or fires a
schedule; only the requested flow runs. That makes invoke the cleanest way to
test a flow. Sourceless flows are also the building block for flow composition
with flow-ref.
Lock it in with a test
Invoking a flow by hand tells you it works right now. A test tells you it still
works after the next edit. The greet flow above already ships with one, and
Unit-Testing a Flow runs it, breaks it on
purpose, and shows what a suite can assert.
Next steps
Continue to the HTTP Quickstart to build a flow that serves real requests, or read Core Concepts for the full model behind flows, sources, and blocks.