Flow configs
Describe a flow as YAML or JSON and join it to your handlers with Flow.
A FlowConfig describes the graph as data: the nodes, the tools each offers, and where each tool
leads. Your code only implements what the tools do.
const YAML = `
initial_node: greeting
nodes:
greeting:
role_message: You work for {{ clinic }}.
task_messages:
- role: developer
content: Greet the caller and ask for their name.
functions:
- name: record_name
transition_to: ask_reason
ask_reason:
task_messages:
- role: developer
content: Ask why {{ name }} is calling.
context_strategy: reset
`;
async function record_name(fm: FlowManager, name: string) {
fm.state.name = name;
// The config decides the next node.
return [{ status: "ok" }, TRANSITION_IN_YAML] as const;
}
Object.assign(record_name, {
description: "Record the caller's name.",
parameters: { name: { type: "string" } },
});
const flow = new Flow(FlowConfig.fromYaml(YAML), { handlers: { record_name } });
const configured = new FlowManager({
worker,
llm,
contextAggregator: aggregators,
globalFunctions: flow.globalFunctions,
});
configured.state.clinic = "Riverside Dental";
await configured.initialize(flow.initialNode);Flow joins the config to the handlers it names. It checks every reference at construction (each
tool exists and is a valid direct function, each action handler is callable) and reports all
problems together in one FlowReferenceError. It then hands the manager ready-made nodes:
flow.initialNode, flow.node(name) and flow.globalFunctions.
A tool in a config returns [result, TRANSITION_IN_YAML]; the config's transition_to picks the
next node. transition_to can also branch on a field of the result:
functions:
- name: select_pizza_order
transition_to:
field: status
cases:
ok: confirm
unavailable: choose_pizza
default: confirmA transition_only function needs no handler at all: give it a description and a transition_to,
and it just moves the conversation.
Loading
FlowConfig.fromYaml(text), fromJson(text) and modelValidate(object) take text or parsed data.
fromFile reads through a function you provide, which keeps the package runtime-agnostic:
const fromDisk = FlowConfig.fromFile("flow.yaml", {
readFile: (path) => readFileSync(path, "utf8"),
});!include in YAML uses the same readFile, resolved against a baseDir. It can read any file the
process can read, so use it only for configs you ship; load untrusted text with fromJson, or with
fromYaml and no baseDir.
Invalid configs throw ValidationError with every problem found, not only the first.
FlowConfig.modelJsonSchema() returns the JSON Schema of the file format, for editor validation.
Placeholders
Prompt text may contain {{ key }} placeholders: a node's role_message, the content of its
task_messages, and the text of tts_say actions. The manager fills them from
flowManager.state every time it enters the node. {{ order.size }} reads into a stored object, and
\{{ key }} shows a literal {{ key }}. A key missing from state throws FlowError on entry.
pipedog is an unofficial community TypeScript port of Pipecat. It is not affiliated with or endorsed by Daily or the Pipecat project.
Portions adapted from the Pipecat documentation, Copyright (c) 2024–2025 Daily, BSD-2-Clause (see apps/docs/LICENSE-pipecat-docs).