Every step in a Tines 3B workflow is its own small program, and workflows need a way to move data from one step to the next. Tines 3B uses the two simplest, most universal channels a program has:
Standard input (stdin), what a program reads
Standard output (stdout), what it writes.
Understanding this one idea explains how data flows through an entire workflow.
Input in, output out
Each step follows the same rhythm. It reads its input from stdin, does its work, and writes its result to stdout. That's the whole contract. Because it's built on the standard streams every language already has, it works the same whether a step is written in TypeScript, Python, or shell:
// TypeScript
const input = await Bun.stdin.text();
const data = JSON.parse(input);
// ... do work ...
console.log(JSON.stringify(result)); // writes to stdout
There's nothing Tines 3B-specific to learn about reading input or writing output. If you know how to read stdin and print to stdout, you know how to move data through Tines 3B.
One step's output becomes the next step's input
Steps are linked together, and the link is the plumbing: when a step finishes, its stdout is handed to the next step as that step's stdin. So the result one step prints is exactly what the following step reads. Chain several steps and data flows down the line, each one transforming what the last produced.
When a step links to more than one downstream step, they all receive the same output and run at the same time. That's how a workflow fans out: one step's result can feed several branches in parallel.
What a step finds on its stdin
Where a step's input comes from depends on how the step is reached:
From an upstream step. The stdin is the raw output the previous step wrote. If several steps feed into it, it gets the most recent successful one.
From an HTTP request. A step triggered by a web request receives the full incoming request, headers and body, on its stdin.
From nothing. A step with nothing upstream simply starts with empty input.
In every case it's the same channel, stdin, so a step reads its input the same way no matter what triggered it.
Writing nothing stops the flow
There's one rule worth knowing well: if a step writes no output at all, the steps after it don't run. This is intentional and genuinely useful.
A step that decides "there's nothing to pass along here" can simply write nothing, and it acts as a filter that quietly stops that branch. When you do want the workflow to continue, write something, even a short "ok," and the downstream steps proceed.
stdout is for data, stderr is for logs
Because stdout is the data channel that feeds the next step, it should carry only the real result. Stray debug prints on stdout would become part of the data and corrupt it.
For logging, notes, and anything meant for a human rather than the next step, use standard error (stderr) instead. Whatever a step writes to stderr is captured as that step's log, kept separate from the data it passes on.
Producing a web response
For workflows that answer web requests, the same stdout is how the response is sent. A step marked as the responding step has its stdout returned to the caller as the HTTP response. So building an API or a page comes down to writing the response to stdout, the same channel you'd use to feed a downstream step.
Why it works this way
Passing data through stdin and stdout keeps steps completely independent. A step doesn't need to know what ran before it or what runs after; it just reads its input and writes its output, like a well-behaved command-line tool. That independence is what lets each step run in its own isolated container and lets you write steps in whichever language fits the job, all while they still connect seamlessly into one workflow.
