This document is the canonical reference for defining Nodes in Trellis. It reflects the v0.7+ Universal Action Semantics ("Duck Typing").
The Node is the atomic unit of the conversation/flow.
- It can Speak (Text).
- It can Listen (Input).
- It can Act (Tool).
- It can Decide (Transition).
We do not rely on rigid type fields (like type: tool). Instead, the properties you define determine the node's behavior.
- Has
do? -> It is an Action Node. The Engine executes the tool. - Has
waitorinput_type? -> It is an Input Node. The Engine pauses for user input. - Has
content? -> It renders text (Markdown).
Note: You can mix behaviors, with constraints.
Content+Do= "Talk & Act" (e.g. "Loading..." + Init).Content+Wait= "Talk & Listen" (e.g. "Question?" + Input).- Forbidden:
Do+Wait(in the same node). You cannot act and listen simultaneously (state collision).
type: text # Optional. Inferred from behavior (Defaults to "text").
# --- Behavior: Action (The "Do") ---
do:
name: my_tool_name # Tool to execute
args: # Arguments passed to the tool
id: "{{ .user_id }}"
# --- Behavior: SAGA (The "Undo") ---
undo:
name: revert_tool # Executed if flow rolls back
args:
id: "{{ .user_id }}"
# --- Context Management ---
required_context: # Fails if these keys are missing
- "user_id"
default_context: # Fallback values
theme: "dark"
context_schema: # Type validation for context values
api_key: string
retries: int
tags: [string]
# --- Behavior: Input (The "Wait") ---
wait: true # Pauses for simple text input (Enter)
# OR
input_type: confirm # Pauses for typed input (e.g., [y/N])
input_options: ["A","B"]
save_to: my_variable # Saves input (or tool result) to Context
# --- Flow Control ---
to: next_node_id # Unconditional transition
# OR
transitions:
- condition: input == 'success'
to: success_node
- to: fallback_node
on_error: error_handler_node # Transition if Tool fails
# Signal Handlers
on_timeout: timeout_node # Syntactic Sugar for on_signal["timeout"]
on_signal:
interrupt: exit_node # Handle specific signals from THIS node
on_signal_default:
interrupt: global_exit # RECOMMENDED: Define on Root Node to handle signals across the entire flow.
quit: cleanup_node
timeout: "30s" # Max time to wait for inputA technical node used for post-processing or converting content (e.g., Markdown to HTML). It utilizes the configured ContentConverter in the engine.
id: docs_render
type: format
format: markdown
message: |
# Hello
This is **markdown**.Nodes of type format typically have a format property specifying the transformation type.
In Markdown files, any text below the frontmatter is the content.
---
do: init_db
to: menu
---
**Initializing Database...**
Please wait while we set up tables.In strictly structured files, use the content key.
id: start
do:
name: init_db
content: "Initializing Database..."
to: menuRender a message and immediately execute a backend task. The transition happens when the task receives a Result.
# Loading Screen
content: "Checking credentials..."
do: check_creds
save_to: auth_result
transitions:
- condition: input.is_valid
to: dashboard
- to: login_failHow it works:
- Engine renders "Checking credentials...".
- Engine executes
check_creds. - Tool returns result
{"is_valid": true}. - Result is saved to
auth_result. - Transition condition
input.is_validis evaluated (True). - Engine moves to
dashboard.
For interactions that require specific choices, use type: question (optional if behaviors are clear) or input_type: choice.
# Simple Yes/No
content: "Proceed?"
input_type: confirm
save_to: proceed
# Multiple Choice (Syntactic Sugar)
content: "Pick a color:"
options:
- "Red"
- "Blue"
transitions:
- condition: input == 'Red'
to: red_pillNote on Options: The
optionslist impliesinput_type: choice. The engine presents these to the user (e.g. arrow keys in CLI).
If an action fails, on_error takes precedence over to.
do: critical_op
on_error: rollback_node
to: success_node- Success: Goes to
success_node. - Failure: Goes to
rollback_node.
Define ad-hoc scripts inline (requires --unsafe-inline) or via tools.yaml.
# Ad-hoc execution (Dev Mode)
do:
name: quick_script
x-exec:
command: python
args: ["scripts/calc.py"]All tool arguments are passed as a single JSON object via the TRELLIS_ARGS environment variable. This provides a unified, language-agnostic interface for tool execution.
Example Flow Node:
do:
name: greet_user
args:
name: "{{ user_name }}"
greeting: "Hello"
config:
debug: trueTool Implementation (PowerShell):
$TrellisArgs = $env:TRELLIS_ARGS | ConvertFrom-Json
$Name = $TrellisArgs.name
Write-Output "Hello, $Name!"Define global signal handlers on your entry node (typically start.md) to handle signals like quit or interrupt from anywhere in the flow.
Example (start.md):
---
id: start
wait: true
to: menu
on_signal_default:
quit: "finish"
interrupt: "catch-interrupt"
---
# Welcome
Press Enter to begin...How It Works:
- User triggers a signal (e.g., types
/quitor pressesCtrl+C) - Engine checks if the current node has an
on_signalhandler for that signal - If not found, engine falls back to
on_signal_defaulton the entry node - If found in
on_signal_default, transitions to the specified node - If not found anywhere, returns
ErrUnhandledSignal
Use Cases:
- Graceful Exit: Always allow
/quitto go to a cleanup node - Interrupt Handling: Catch
Ctrl+Cto save state before exiting - Timeout Fallback: Global timeout handler for all idle states
Best Practice: Define on_signal_default on your root node (start) to provide consistent signal handling across your entire flow.
Trellis v0.7.10+ uses a tiered shutdown strategy (SIGTERM -> Grace Period -> SIGKILL). For tools to benefit from this, they should be "Good Citizens":
- Handle Signals: Listen for
SIGTERM(andSIGINTfor local dev). - Cleanup: On signal, flush buffers, save state, and exit promptly.
- Result Delivery: Always write the final result (JSON or text) to
stdout.
Example (Python):
import signal
import sys
import json
def handle_shutdown(signum, frame):
# Perform cleanup here
sys.exit(0)
signal.signal(signal.SIGTERM, handle_shutdown)
# ... perform tool logic ...
print(json.dumps({"status": "success"}))| Property | Type | Description |
|---|---|---|
do |
ToolCall |
Definition of side-effect to execute. |
wait |
bool |
If true, pause for user input (default text). |
content |
string |
Message to display to the user. |
options |
[]string |
Shorthand for choice input. Presents a menu. |
input_type |
string |
text (default), confirm, choice, int. |
input_default |
string |
Default value if user presses Enter. |
input_options |
[]string |
Options for choice input (Low-level). |
messages |
map[string]string |
Map of locales to content. Used for internationalization. See I18n Guide. |
next |
string |
The ID of the next node to transition to. |
save_to |
string |
Context variable key to store Input or Tool Result. |
to |
string |
Shorthand for single unconditional transition. |
transitions |
[]Transition |
List of conditional paths. Evaluated in order. |
on_error |
string |
Target node ID if do fails. |
on_timeout |
string |
Syntactic sugar for on_signal["timeout"]. |
on_interrupt |
string |
Syntactic sugar for on_signal["interrupt"]. |
on_signal |
map[string]string |
Handlers for global signals (interrupt, timeout). |
on_signal_default |
map[string]string |
Global signal handlers (valid only on Root/Entry node). |
tools |
[]Tool |
Definitions of tools available to this node (for LLMs). |
undo |
ToolCall |
SAGA compensation action if flow rolls back. |
required_context |
[]string |
Keys that MUST exist in context or flow errors. |
default_context |
map[string]any |
Default values for context keys if missing. |
context_schema |
map[string]string |
Type constraints for context values (fail fast on mismatch). |
timeout |
string |
Duration (e.g. "30s") to wait for input before signaling timeout. |
Use context_schema to validate types in the context before a node renders. Supported types:
string,int,float,bool- Slice types like
[string],[int]
context_schema:
api_key: string
retries: int
tags: [string]If a value is missing or has the wrong type, execution fails with a ContextTypeValidationError.
When using input_type: confirm, Trellis follows the standard CLI convention:
- Empty Input (Enter): Defaults to
yes(True) unless an explicitinput_defaultis provided. - Strict Validation: Only
y,yes,true,1(True) orn,no,false,0(False) are accepted. - Normalization: Input is automatically converted to a canonical
yesornobefore being saved tosave_toor evaluated intransitions.
Implementation Example:
input_type: confirm
input_default: "yes" # Overrides convention to make Enter = True
on_denied: stop_flow
to: continue_flowNode content and tool arguments support Go's text/template syntax for dynamic interpolation.
---
to: greet
save_to: username
---
What is your name?---
to: done
---
Hello, {{ .username }}!
Welcome back, {{ default "friend" .nickname }}.After a tool node executes successfully, the result is automatically added to the context under tool_result.
If the tool returns a JSON object (Map/Dictionary):
The fields are flattened into tool_result for direct access, and the call ID is stored in _id:
---
to: next_step
---
## Last Tool Call
- Call ID: {{ .tool_result._id }}
- Status: {{ .tool_result.status }}
- Message: {{ .tool_result.message }}
{{ if eq .tool_result.status "success" }}Operation succeeded.{{ end }}If the tool returns a scalar (String, Int, etc.):
The result is stored in the result field, and the call ID is stored in _id:
- Call ID: {{ .tool_result._id }}
- Result: {{ .tool_result.result }}| Function | Example | Description |
|---|---|---|
default |
{{ default "N/A" .key }} |
Returns .key if non-zero, otherwise the fallback |
coalesce |
{{ coalesce .a .b .c }} |
Returns the first non-zero value |
toJson |
{{ toJson .obj }} |
Serializes to JSON string |
index |
{{ index .map "key" }} |
Accesses a map by dynamic key (built-in) |
For the full reference — including
HTMLInterpolatorfor browser output, reserved keys, and known limitations — see docs/reference/interpolation.md.