Handles
Understand node handles in ActionFlows: the connection points that let you wire nodes together and control how data and execution flow across the canvas.
Handles
The Type System for Data Flow
Handles are typed connection points between nodes. Every handle declares what data it accepts or produces, enabling design-time validation instead of runtime debugging.
What is a Handle
A handle is a contract between two nodes:
- Input handle (left): What data this node needs
- Output handle (right): What data this node produces
- Type: The shape and structure of that data (Text, Number, Object, Array, Boolean)
Think of handles like function signatures in TypeScript. They enforce correctness before execution.
// Function signature
function generateContent(prompt: string, model: "claude" | "gpt4"): {text: string, tokens: number}
// Handle equivalent
Generate Text Node
├─ Input: prompt (Text), model (Enum)
└─ Output: text (Text), tokens (Number)Input vs Output Handles
Input Handles (Left Side)
Data flows into the node.
Three ways to provide input:
1. Connected Input: Output from another node
Start node "body" output
↓ (flows through connection)
Generate Text "prompt" input2. Hardcoded Input: Static value on the node
Model: "claude-3.5-sonnet" (hardcoded, never changes)
Temperature: 0.7 (hardcoded, never changes)3. Run-time input: Empty required fields on downstream nodes
Run flow → Flow inputs
├─ {generateTextNodeId}_prompt (Text)
└─ other empty required fields as {nodeId}_{inputName}Required vs Optional
- Required: Must be connected or hardcoded
- Optional: Can be omitted (defaults apply)
Output Handles (Right Side)
Data flows out of the node.
By node type:
| Node | Outputs | Types |
|---|---|---|
| Generate Text | text, token_count, cost | Text, Object, Number |
| Generate Image | image_url, cost, metadata | Text, Number, Object |
| If (condition) | true, false | Execution paths |
| Loop | item, index, completed | Object, Number, Signal |
| Database Read | record, records, error | Object, Array, Object |
| Slack Send | message_id, status, error | Text, Text, Object |
| Error path | error | Object |
Every node has at least one output. Many have multiple.
Type System: Why It Matters
The Problem: Loose Types (n8n, Make, Zapier approach)
Traditional automation platforms treat data as untyped JSON:
// Node A outputs anything
{
"result": "hello",
"count": 5,
"metadata": {...}
}
// Node B expects specific shape
// Do they match? Platform doesn't know until runtime.
// At runtime: Error or silently failsResult: Bugs at execution time. You debug in production.
The Solution: Typed Handles (ActionFlows approach)
ActionFlows enforces types at design time:
Node A declares output: {result: Text, count: Number}
Node B declares input: {result: Text, count: Number}
Platform validates: Do these match?
✓ Yes → Connection enabled, proceed
✗ No → Connection disabled, error shown
Bug caught before execution.This is static type checking for workflows. Like TypeScript preventing bugs before runtime.
Type Matching Rules
Primitive Types
Text ↔ Text ✓
Number ↔ Number ✓
Boolean ↔ Boolean ✓
Array ↔ Array ✓
Date ↔ Date ✓
Text ↔ Number ✗
Number ↔ Array ✗
Boolean ↔ Object ✗Incompatible connections appear disabled and dimmed on canvas.
Complex Types: Objects
For Object types, structure must align:
// Node A output
{
name: Text,
age: Number,
email: Text
}
// Node B input (scenario 1)
{
name: Text,
age: Number,
email: Text
}
Match ✓ (exact match)
// Node B input (scenario 2)
{
name: Text,
age: Number
}
Match ✓ (Node B doesn't use email, extra fields ignored)
// Node B input (scenario 3)
{
name: Text,
age: Number,
email: Text,
phone: Text (required)
}
Mismatch ✗ (Node B requires phone, but Node A doesn't output it)Rule: Extra fields are fine. Missing required fields are errors.
Arrays
Array<Text> ↔ Array<Text> ✓
Array<Object> ↔ Array<Object> ✓
Text ↔ Array<Text> ✗
Number ↔ Array<Number> ✗Data Flow Through Handles
Explicit Mapping (Not Implicit)
When you connect two nodes, you explicitly choose which output feeds which input. No guessing.
Generate Text node outputs:
├─ text (Text)
├─ token_count (Object: {input: Number, output: Number})
└─ cost (Number)
↓ (you choose what to use)
Slack node inputs:
├─ channel (Text): hardcoded to "#updates"
├─ message (Text): connected to Generate Text "text"
└─ thread_id (Text, optional): left unconnected
Result: Only "text" flows to Slack. Other outputs unused.You control the flow explicitly. Nothing is automatic.
Variable references
Map another node's output into a text field by dropping an output chip. The stored expression is {{nodeId.outputName}} (legacy {nodeName.output} still resolves).
Prompt on Generate Text:
Analyze this customer feedback: {{startNodeId.body}}Type-ahead and drag from output handles insert the chip. Do not invent an Email Trigger node name in the expression.
Handle Debugging
Inspect Values in Run History
View what actually flowed through each handle during execution:
Click any node → See full execution details
Inputs provided:
├─ prompt: "Summarize this article in 50 words"
├─ model: "claude-3.5-sonnet"
└─ temperature: 0.7
Outputs produced:
├─ text: "This article discusses AI automation..."
├─ token_count: {input: 145, output: 287}
└─ cost: 0.003
Status: ✓ Success
Duration: 1,240msEvery execution is logged with full input/output transparency.
Type Mismatch Error
Error: Cannot connect "Array<Object>" output to "Text" input
Node: Database Read produces Array<Object>
Node: Email expects Text
Solution: Use a Transform node to convert Array to Text
or connect Array output to a node that accepts ArraysMissing Required Input
Error: Slack node missing required input "message"
Slack requires "message" (Text)
It's not connected to any node output
It's not hardcoded with a value
Solution: Connect an output OR hardcode a valueHandle Anatomy: Real Example
Start node (webhook body or Flow inputs)
├─ Outputs:
├─ from (Text): "[email protected]"
├─ subject (Text): "Help needed"
└─ body (Text): "I can't log in..."
↓ (data flows through connections)
Generate Text Node
├─ Inputs:
├─ model: "claude-3.5-sonnet" (hardcoded)
├─ prompt: "Classify this email: {startBody}"
└─ temperature: 0.7 (hardcoded)
├─ Outputs:
├─ text (Text): "Classification: Technical Issue"
├─ token_count (Object): {input: 89, output: 12}
└─ cost (Number): 0.0008
↓ (classification flows downstream)
If Node (decision)
├─ Inputs:
├─ condition: "{generateText.text} contains 'Technical'"
└─ (evaluated at runtime)
├─ Outputs:
├─ true: (execution path)
└─ false: (execution path)
↓ (true branch only executes)
Slack Node (technical issues)
├─ Inputs:
├─ channel: "#tech-support" (hardcoded)
├─ message: mapped from start body / subject chips
└─ thread: (optional, not used)
├─ Outputs:
├─ message_id: "1729xxx"
└─ status: "success"This is the complete flow of handles from trigger to action.
Best Practices
1.Always Map Explicitly
✓ Good:
Start body / Flow inputs → Generate Text.prompt
(Clear, intentional)
✗ Bad:
Generate Text.prompt (empty, hoping context flows)
(Implicit, ambiguous)2.Use Semantic Handle Names
✓ Good: customer_email_address, generated_blog_title, approval_status
✗ Bad: data1, output_value, temp, x, ySemantic names tell you what data represents. One-letter names tell you nothing.
3.Handle Errors Explicitly
Generate Text
├─ text output → Slack (happy path)
├─ error output → Error handler (sad path)
├─ Log error
├─ Retry or alertEvery node that can fail should have explicit error routing.
4. Test types in Actionflow Studio
Before deploying, verify handles work with realistic data:
1. Hardcode all inputs to test values
2. Run in Actionflow Studio
3. Inspect outputs
4. Check types match
5. Then deploy5.Document Complex Objects
For complex Object schemas, add notes on what each field means:
Node: Classify Customer
Output: {
"segment": "premium" | "standard" | "churn_risk" (required)
"confidence": 0-1 (required, confidence score)
"reasoning": string (optional, for debugging)
}Limitations
Static Type Declaration
Handles are declared at design time. You can't dynamically create handles based on data.
❌ Not possible: Create handles based on JSON keys
✓ Workaround: Use fixed Object schema or Array<Object>Handle Count
Too many handles = bad design. Keep total inputs + outputs to 10-15 max.
❌ Node with 50 handles = design smell
✓ Refactor: Combine related inputs into one ObjectNo Implicit Coercion
Types must match exactly. No automatic conversion.
❌ Text automatically converts to Number? No
✓ Solution: Use explicit Transform node to convertAdvanced Patterns
Streaming Outputs
Generate Text supports streaming:
Node starts producing text immediately
Character-by-character appears in real-time
Final output fires when complete
Downstream nodes can consume partial output
Useful for UI feedback before full completionMultiple Outputs from Same Handle
One output can feed multiple downstream nodes:
Generate Text "text" output
├─ → Slack (sends message)
├─ → Database (logs result)
└─ → Email (emails recipient)
Same output, three consumersParallel Execution
Multiple branches execute simultaneously if independent:
Start
├─ Branch 1: AI analysis (path 1)
├─ Branch 2: Database query (path 2)
├─ Branch 3: API call (path 3)
↓ (all three run in parallel)
Merge node (waits for all three)
↓
Continue (receives results from all branches)Merge waits for all inputs before proceeding.
Next Steps
Node Catalog: See handles on every node type
Inputs: How nodes receive data
Actionflow Studio: Connect handles on the canvas, then run
Node Requirements: Rules that keep connections valid
Handles define how data and execution move between nodes.
Node Anatomy
Understand the structure of an ActionFlows node: its inputs, outputs, handles, configuration, and how data flows through it on the canvas.
Node Inputs
Learn how node inputs work in ActionFlows (static values, expressions, and data mapped from upstream nodes) to pass data through your flows.