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 driven
by a schedule, and one you call directly from the command line. Together they
show the two ways a flow receives work. 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: greeterReading it top to bottom:
connectorsdeclares acronconnector namedticker. Connectors own the machinery a flow needs to talk to the outside world.processorsdefines a reusable, named block.greeteris alogblock whosemessagesetting is a CEL expression ā it can read the messagebody,vars,eventID, andcorrelationID.flowsbinds it together. Thesourceattaches the flow to thetickerconnector: a six-field cronschedule(with seconds) fires at seconds 0 and 30 of every minute, and thepayloadCEL expression ā withnowbound to the fire time ā becomes each message's body. Theprocesschain has one step, which references the named processor withref: 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 logs its startup,
prints a ready banner, then logs a greeting every 30 seconds:
time=... level=INFO msg="starting runtime" version=0.4.2 connectors=1 flows=1š octo v0.4.2 is up and ready to roll!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 ā edit
the schedule or the message, save, and watch it take effect without a
restart.
Invoking a flow directly
Not every flow needs a schedule or a port. 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 + "!"}'The set-payload block 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, not just the payload: its
event_id, the variables the flow set (none here), and the body. Pipe it
through jq '.body' when the body is all you are after.
When --data is omitted, invoke reads the body from stdin, so you can pipe
into it:
echo '{"name":"Grace"}' | bin/octo invoke --config samples/hello-invoke.yaml --flow greetIn invoke mode no sources are started ā nothing binds a port or fires a
schedule; only the requested flow runs, and its result message prints as JSON.
This makes invoke the cleanest way to test a flow, and sourceless flows are
also the building block for flow composition with flow-ref.
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.