Skip to main content

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" input

2. 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:

NodeOutputsTypes
Generate Texttext, token_count, costText, Object, Number
Generate Imageimage_url, cost, metadataText, Number, Object
If (condition)true, falseExecution paths
Loopitem, index, completedObject, Number, Signal
Database Readrecord, records, errorObject, Array, Object
Slack Sendmessage_id, status, errorText, Text, Object
Error patherrorObject

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 fails

Result: 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,240ms

Every 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 Arrays

Missing 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 value

Handle 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, y

Semantic 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 alert

Every 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 deploy

5.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 Object

No Implicit Coercion

Types must match exactly. No automatic conversion.

❌ Text automatically converts to Number? No
✓ Solution: Use explicit Transform node to convert

Advanced 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 completion

Multiple 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 consumers

Parallel 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.

On this page