Serialization & YAML¶
Save and load graphs as YAML files or Python dicts. A saved graph loads back as the same graph: every node setting round-trips, or saving refuses with an error that names the node and the setting.
Save a Graph¶
from pydantic import BaseModel
from promptise.engine import PromptGraph, PromptNode, ValidateNode
from promptise.engine.serialization import save_graph
class Verdict(BaseModel):
passes: bool
issues: list[str]
graph = PromptGraph("checked-answer", mode="static")
graph.add_node(PromptNode("answer", inject_tools=True, default_next="verify"))
graph.add_node(ValidateNode(
"verify", output_schema=Verdict, on_pass="__end__", on_fail="answer", max_iterations=3,
))
graph.set_entry("answer")
save_graph(graph, "my-agent.yaml")
The file is written only if the whole graph can be represented (see What can't be saved).
Load a Graph¶
from promptise.engine.serialization import load_graph
graph = load_graph("my-agent.yaml", tools=agent.tools, refs=[Verdict])
| Argument | What it's for |
|---|---|
tools |
Tools are saved by name. Pass the tools to resolve them against — a list (typically agent.tools) or a {name: tool} mapping. A name that isn't provided raises. |
refs |
Python objects the file references by import path — output_schema models, processors, edge conditions, @node functions. Pass them as a list (or a {"module:QualName": obj} mapping). References into promptise itself resolve without it. |
allow_imports |
Import any other reference instead of requiring refs. Importing a module runs its code — only for files you trust. Default False. |
The file may hold the graph at the top level or under a graph key.
What Round-Trips¶
| Setting | Stored as |
|---|---|
Instructions, transitions, default_next, max_iterations, metadata |
Plain data |
Every node type's own settings — input_keys, output_key, inherit_context_from, context_scope, temperature, on_pass/on_fail, max_subgoals, … |
Plain data |
Flags (including inject_tools, is_entry, is_terminal) |
flags: [inject_tools, ...] |
compaction (a ContextCompaction, or True/False/an int) |
Its fields, e.g. compaction: {after_tool_results: 3} or compaction: {reuse_results: [get_user]} (a custom count_tokens as an import reference) |
| Tools | Tool names, resolved from tools= on load |
output_schema, preprocessor, postprocessor, transform, merge_fn, tool_selector, a LoopNode condition |
Import reference "module:QualName" |
model_override given as a string |
The model id |
Nested nodes (ParallelNode, LoopNode, RetryNode, FanOutNode, AutonomousNode pool) and subgraphs |
Nested configs |
An @node function |
A reference to the module-level node |
Graph name, mode, entry, edges |
Plain data |
Edges from on_tool_call, on_no_tool_call, on_output, on_error, on_confidence, on_guard_fail |
A condition: mapping |
Edges from when(...) / loop_until(...) with a module-level function |
condition: {ref: "module:function"} |
loop_until(..., max_iterations=...) budgets |
loops: {refine: {max_iterations: 5, exit_to: deliver}} |
Values a node would get by default are left out, so a saved graph stays short and picks up improved defaults (for example a reasoning node's built-in instructions).
What Can't Be Saved¶
These are Python objects with state of their own. Saving a graph that uses them raises GraphSerializationError; remove them before saving and set them in code after loading:
- A lambda, or a function or class defined inside another function (as an
output_schema, processor or edge condition) — move it to module level. An object defined in the script you run is saved as__main__:Name; pass it inrefs=when loading. blocks,strategy,perspective,guards, aRouterNode'scontext_blocks.- A
model_overridethat is a model object rather than a model id string (it may carry credentials). - A node class that isn't registered (see Custom Node Types).
from promptise.engine.serialization import GraphSerializationError, save_graph
try:
save_graph(graph, "agent.yaml")
except GraphSerializationError as exc:
print(exc)
# Edge act → verify condition: can't be saved — it is a lambda or defined
# inside a function. Define it at module level in an importable module, ...
Loading is just as strict: an unknown node type, an unknown field, a tool that wasn't provided or a reference that can't be resolved raises GraphSerializationError (a ValueError).
YAML Format¶
version: 2
name: plan-act-verify
mode: static
entry: plan
nodes:
plan:
type: plan
instructions: Plan how to answer with the sales tools.
output_schema: my_agent.schemas:Plan
flags: [observable, stateful]
act:
type: prompt
instructions: Work through the plan with your tools, then write the answer.
flags: [inject_tools] # receives the agent's tools at run time
max_iterations: 8
verify:
type: validate
output_schema: my_agent.schemas:Verdict
on_pass: __end__
on_fail: reflect
max_iterations: 3
flags: [readonly, validate_output]
reflect:
type: reflect
input_keys: [validation]
flags: [observable, stateful]
edges:
- from: plan
to: act
- from: act
to: verify
- from: reflect
to: act
- from: verify
to: reflect
condition: {kind: output, key: passes, value: false}
A node with flags: [inject_tools] (or inject_tools: true) gets the agent's tools when the graph runs, so the file doesn't need to name them. Hand-written files may also use is_entry: true / is_terminal: true.
Edge Config Format¶
| Field | Type | Required | Description |
|---|---|---|---|
from |
str |
Yes | Source node name |
to |
str |
Yes | Target node name |
label |
str |
No | Label for visualization |
priority |
int |
No | Priority for condition checking (higher = checked first) |
condition |
mapping | No | {kind: tool_called}, {kind: no_tool_call}, {kind: error}, {kind: guard_failed}, {kind: output, key: ..., value: ...}, {kind: confidence, min_confidence: 0.7}, or {ref: "module:function"} |
The built-in conditions are EdgeCondition objects (from promptise.engine import EdgeCondition), so graph.when("a", "b", condition=EdgeCondition("output", key="done", value=True)) saves too.
Python Dict Format¶
from promptise.engine.serialization import graph_from_config, graph_to_config
config = graph_to_config(graph) # plain data, with "version": 2
graph = graph_from_config(config, tools=agent.tools, refs=[Verdict])
node_to_config(node) and node_from_config(config, tools=..., refs=..., allow_imports=...) do the same for a single node.
Node Type Registry¶
All built-in node types are registered:
| Type | Node Class | Category |
|---|---|---|
prompt |
PromptNode | Standard |
tool |
ToolNode | Standard |
router |
RouterNode | Standard |
guard |
GuardNode | Standard |
parallel |
ParallelNode | Standard |
loop |
LoopNode | Standard |
human |
HumanNode | Standard |
transform |
TransformNode | Standard |
subgraph |
SubgraphNode | Standard |
autonomous |
AutonomousNode | Standard |
code_action |
CodeActionNode | Standard |
think |
ThinkNode | Reasoning |
reflect |
ReflectNode | Reasoning |
observe |
ObserveNode | Reasoning |
justify |
JustifyNode | Reasoning |
critique |
CritiqueNode | Reasoning |
plan |
PlanNode | Reasoning |
synthesize |
SynthesizeNode | Reasoning |
validate |
ValidateNode | Reasoning |
retry |
RetryNode | Reasoning |
fan_out |
FanOutNode | Reasoning |
A CodeActionNode built by build_agent(agent_pattern="code-action") holds a sandbox factory created at run time, so it can't be saved; one you build with a module-level factory can.
Custom Node Types¶
Register custom node types so they can be saved and loaded:
from promptise.engine.serialization import register_node_type
class AuditedNode(PromptNode):
"""A PromptNode subclass with no constructor parameters of its own."""
register_node_type("audited", AuditedNode)
A registered subclass of a built-in node keeps all of the built-in's settings. If its constructor takes parameters of its own, define to_config() and from_config() so they are saved too — otherwise saving raises rather than dropping them. from_config() receives the node's whole config, base fields such as instructions and default_next included:
class DatabaseNode(BaseNode):
def __init__(self, name: str, *, table: str, **kwargs):
super().__init__(name, **kwargs)
self.table = table
def to_config(self) -> dict:
return {"table": self.table, "default_next": self.default_next}
@classmethod
def from_config(cls, config: dict) -> "DatabaseNode":
return cls(config["name"], table=config["table"], default_next=config.get("default_next"))
register_node_type("database", DatabaseNode)