Hello, Mélodium
Source: tutorial/01_hello_melodium See in Playground
The smallest useful Mélodium program. It has no procedural entry point: main is a graph of treatments connected together, not a function body that runs top to bottom. startup() fires once when the engine starts, and every other treatment reacts as soon as the data it needs is available.
Running
cd tutorial/01_hello_melodium
melodium run Compo.toml --name "World" --times 3The program computes a single greeting with a pure function and writes it to hello.txt (the Block half), and generates the same name repeated times times as a stream, numbering each occurrence (the Stream half). Both are logged, so a run shows the two execution styles side by side.
Optional: add --api-report and an API token (MELODIUM_API_TOKEN) to see this run’s full trace on Cadence.CI .
How it works
This example uses no models: only stateless treatments and functions.
Data flow
startup.trigger fires once and fans out to both halves of the graph: nothing here runs “first” or “second”, both start as soon as the trigger arrives.
The Block side: one greeting, written once
|format and |map/|entry build the greeting string from the name parameter, purely, with no I/O involved. emit turns it into a Block<string>, and stream widens it into a one-element Stream<string> so writeTextLocal can consume it:
greeting: emit<string>(value=|format("Hello, {name}! Welcome to Mélodium.", |map([|entry("name", name)])))
asText: stream<string>()
write: writeTextLocal(path=output)
logWritten: logInfoMessage(label="hello", message="greeting written to file")
startup.trigger -> greeting.trigger,emit -> asText.block,stream -> write.text
write.finished -> logWritten.triggerstd/flow::stream is the standard way to turn a one-shot value into a one-element stream when a downstream treatment expects a Stream rather than a Block.
The Stream side: the same name, repeated and numbered
emit<u128>(value=times) supplies the length to generate, which repeats name that many times as a Stream<string>. That stream fans out directly to logInfos, and separately through count and toString before being logged again:
howMany: emit<u128>(value=times)
names: generate<string>(data=name)
tally: count<string>()
tallyText: toString<u128>()
logNames: logInfos(label="greeting-stream")
logCount: logInfos(label="greeting-count")
startup.trigger -> howMany.trigger,emit -> names.length,stream -> tally.stream
names.stream -> logNames.messages
tally.count -> tallyText.value,into -> logCount.messagescount numbers elements starting at 1, so a run with --times 3 logs 1, 2, 3.
Functions vs treatments
|format, |map, |entry are pure functions: they have no ports and are called inline as values. emit, stream, generate, count are treatments: they have ports and live in the graph. Mélodium functions take positional arguments in their declared order, e.g. |format(format_string, entries), which does not always match alphabetical order, so when in doubt check a working example or validate with melodium check.
Dependencies
[dependencies]
std = "0.10.3" # core flows, logging, data structures
fs = "0.10.3" # local file I/O