Workflow Nodes
Nodes are the building blocks of workflows. Each node performs a specific function and can be connected to other nodes to create automation sequences.
Node Anatomy
Every node has:
┌─────────────────────────────────────┐
│ ○ Input Handle(s) │
├─────────────────────────────────────┤
│ [Icon] Node Name │
│ Description text... │
│ ⚙️ Configured / ⚠️ Not configured │
├─────────────────────────────────────┤
│ Output Handle(s) ○│
└─────────────────────────────────────┘
- Input Handles: Where data flows in (left side)
- Output Handles: Where data flows out (right side)
- Configuration: Node-specific settings
- Category Badge: Visual indicator of node type
Node Categories
Triggers (Green)
Triggers start workflow execution. Every workflow needs at least one trigger.
Characteristics:
- No input handles (they start the flow)
- One or more output handles
- Provide initial data to the workflow
- Marked as
is_entry_pointin database
Actions (Blue)
Actions perform operations - send emails, make API calls, update records.
Characteristics:
- One input handle
- One output handle
- May have side effects (sending emails, HTTP requests)
- Can be async (delays, waiting for responses)
Conditions (Yellow)
Conditions branch the workflow based on logic.
Characteristics:
- One input handle
- Multiple output handles (one per branch)
- Evaluate input data against rules
- Output includes
_branchindicator
Transformers (Purple)
Transformers modify data without side effects.
Characteristics:
- One input handle
- One output handle
- Pure functions (same input = same output)
- Used for data mapping, filtering, formatting
Built-in Nodes Reference
Manual Trigger
Identifier: workflows:manual-trigger
Category: Trigger
Starts workflow when user clicks Run or calls the execute API.
Configuration:
| Field | Type | Description |
|---|---|---|
sampleData |
object | Sample data for testing |
Output:
{
"trigger_type": "manual",
"triggered_at": "2025-01-16T10:00:00Z",
"triggered_by": "user-uuid",
...triggerData
}
Schedule Trigger
Identifier: workflows:schedule-trigger
Category: Trigger
Runs workflow on a cron schedule.
Configuration:
| Field | Type | Description | Example |
|---|---|---|---|
cron |
string | Cron expression | 0 9 * * * (daily at 9am) |
timezone |
string | Timezone for schedule | Asia/Kolkata |
Common Cron Patterns:
* * * * * Every minute
0 * * * * Every hour
0 9 * * * Daily at 9:00 AM
0 9 * * 1 Every Monday at 9:00 AM
0 0 1 * * First day of month at midnight
*/15 * * * * Every 15 minutes
Output:
{
"trigger_type": "schedule",
"scheduled_time": "2025-01-16T09:00:00Z",
"cron": "0 9 * * *"
}
Webhook Trigger
Identifier: workflows:webhook-trigger
Category: Trigger
Receives HTTP POST requests to trigger the workflow.
Configuration:
| Field | Type | Description |
|---|---|---|
requireSignature |
boolean | Validate webhook signature |
signatureHeader |
string | Header containing signature |
signatureSecret |
string | Secret for HMAC validation |
Webhook URL:
POST https://your-domain.com/api/webhooks/workflows/{workflow-token}
Output:
{
"trigger_type": "webhook",
"method": "POST",
"headers": { ... },
"query": { ... },
"body": { ...posted_data }
}
Send Email
Identifier: workflows:send-email
Category: Action
Sends an email using the configured mail provider.
Configuration:
| Field | Type | Required | Description |
|---|---|---|---|
to |
string | Yes | Recipient email (supports expressions) |
subject |
string | Yes | Email subject |
body |
string | Yes | Email body content |
bodyType |
enum | No | text or html (default: text) |
Expression Example:
to: "{{customer.email}}"
subject: "Order {{order.id}} Confirmation"
body: |
Hi {{customer.name}},
Your order #{{order.id}} has been confirmed.
Total: ${{order.total}}
Thank you!
Output:
{
...input,
"_email_sent": true,
"_email_to": "john@example.com",
"_email_subject": "Order 123 Confirmation",
"_sent_at": "2025-01-16T10:30:00Z"
}
HTTP Request
Identifier: workflows:http-request
Category: Action
Makes HTTP requests to external APIs.
Configuration:
| Field | Type | Required | Description |
|---|---|---|---|
method |
enum | Yes | GET, POST, PUT, PATCH, DELETE |
url |
string | Yes | Request URL (supports expressions) |
headers |
object | No | Request headers |
body |
string | No | Request body (for POST/PUT/PATCH) |
bodyType |
enum | No | json, form, raw |
timeout |
number | No | Timeout in seconds (default: 30) |
retryOnError |
boolean | No | Retry on HTTP errors |
Output:
{
...input,
"_http_status": 200,
"_http_response": { ...response_body },
"_http_headers": { ...response_headers },
"_http_duration_ms": 234
}
Delay
Identifier: workflows:delay
Category: Action
Pauses workflow execution for a specified duration.
Waits longer than workflows.delay.pause_after_seconds (default 30) do not
occupy a worker. The run is checkpointed, suspended with a due time, and woken
by the workflows:resume-due sweeper — which runs once a minute, so a long
delay elapses no earlier than requested and up to about a minute later. Shorter
waits are still served in process, where that granularity would be worse than
the wait itself. See Execution for detail.
Configuration:
| Field | Type | Required | Description |
|---|---|---|---|
duration |
number | Yes | Delay amount |
unit |
enum | Yes | seconds, minutes, hours |
Example:
duration: 5
unit: minutes
Output:
{
...input,
"_delayed": true,
"_delay_seconds": 300
}
Set Variable
Identifier: workflows:set-variable
Category: Action
Stores a value in the workflow execution context.
Configuration:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Variable name |
value |
any | Yes | Value to store (supports expressions) |
Example:
name: "discount_percentage"
value: "{{order.total > 100 ? 10 : 0}}"
Output:
{
...input,
"_variable_set": "discount_percentage",
"_variable_value": 10
}
Variables are accessible in subsequent nodes via {{variables.discount_percentage}}.
If/Else
Identifier: workflows:if-else
Category: Condition
Binary branching based on a condition.
Configuration:
| Field | Type | Required | Description |
|---|---|---|---|
condition |
string | Yes | Expression that evaluates to boolean |
operator |
enum | No | Comparison operator |
value |
any | No | Value to compare against |
Output Handles:
true- When condition is truefalse- When condition is false
Example Conditions:
// Simple comparison
{{order.total}} > 100
// String matching
{{customer.type}} == "premium"
// Array check
{{items.length}} > 0
// Nested property
{{order.shipping.method}} == "express"
Output:
{
...input,
"_branch": "true", // or "false"
"_condition_result": true
}
Switch
Identifier: workflows:switch
Category: Condition
Multi-way branching based on value matching.
Configuration:
| Field | Type | Required | Description |
|---|---|---|---|
value |
string | Yes | Expression to evaluate |
cases |
array | Yes | List of case values |
defaultCase |
string | No | Default output if no match |
Example:
value: "{{order.status}}"
cases:
- "pending"
- "processing"
- "shipped"
- "delivered"
defaultCase: "other"
Output Handles: One handle per case plus optional default.
Output:
{
...input,
"_branch": "processing",
"_switch_value": "processing"
}
Map
Identifier: workflows:map
Category: Transformer
Transforms the structure of input data.
Configuration:
| Field | Type | Required | Description |
|---|---|---|---|
mapping |
object | Yes | Key-value mapping of output fields |
keepOriginal |
boolean | No | Keep unmapped fields |
Example:
mapping:
email: "{{customer.email}}"
name: "{{customer.first_name}} {{customer.last_name}}"
orderTotal: "{{order.total}}"
keepOriginal: false
Input:
{
"customer": {
"email": "john@example.com",
"first_name": "John",
"last_name": "Doe"
},
"order": { "total": 99.99 }
}
Output:
{
"email": "john@example.com",
"name": "John Doe",
"orderTotal": 99.99
}
Filter
Identifier: workflows:filter
Category: Transformer
Filters items in an array based on conditions.
Configuration:
| Field | Type | Required | Description |
|---|---|---|---|
arrayPath |
string | Yes | Path to array in input |
condition |
string | Yes | Filter condition per item |
outputPath |
string | No | Where to put filtered array |
Example:
arrayPath: "items"
condition: "{{item.price}} > 50"
outputPath: "expensive_items"
Input:
{
"items": [
{ "name": "A", "price": 30 },
{ "name": "B", "price": 75 },
{ "name": "C", "price": 120 }
]
}
Output:
{
"items": [...],
"expensive_items": [
{ "name": "B", "price": 75 },
{ "name": "C", "price": 120 }
],
"_filtered_count": 2,
"_original_count": 3
}
Creating Custom Nodes
See Extending Workflows for a complete guide to creating custom nodes.
Quick Example
<?php
namespace Modules\MyModule\Nodes\Actions;
use Modules\Workflows\Nodes\BaseNode;
class MyCustomNode extends BaseNode
{
public static function getIdentifier(): string
{
return 'my-module:custom-action';
}
public static function getName(): string
{
return 'My Custom Action';
}
public static function getCategory(): string
{
return 'action';
}
public static function getConfigSchema(): array
{
return [
'type' => 'object',
'properties' => [
'myField' => [
'type' => 'string',
'title' => 'My Field',
],
],
'required' => ['myField'],
];
}
public function execute(array $input, array $config, array $context): array
{
// Your logic here
return array_merge($input, [
'_custom_result' => 'done',
]);
}
}
That is the whole node. The builder renders the configuration form from
getConfigSchema() — there is no frontend code to write, and nothing to register
in the Next.js app.
Node Configuration
Each node's config panel is generated from its schema. NodeRegistry normalises
whatever the node declared and serves it as config_schema; the panel renders that.
Two formats are accepted:
- JSON Schema (
['type' => 'object', 'properties' => [...]]) — preferred, and what validation reads - A flat field list (
[['key' => ..., 'type' => ..., 'label' => ...]]) — used by several existing nodes and still supported
Presentation is controlled declaratively with x-ui: which control to use, how to
group and order fields, and when a field should appear at all.
'body' => [
'type' => 'object',
'title' => 'Request Body',
'x-ui' => [
'group' => 'Advanced',
'showIf' => ['field' => 'method', 'in' => ['POST', 'PUT', 'PATCH']],
],
],
A field hidden by showIf is not validated and its value is dropped on save, so a
request switched back to GET does not keep a body it will never send.
See Extending Workflows
for the full widget mapping, every x-ui key, and the validation rules.
Node Connection Rules
Valid Connections
| Source Category | Can Connect To |
|---|---|
| Trigger | Action, Condition, Transformer |
| Action | Action, Condition, Transformer |
| Condition | Action, Condition, Transformer |
| Transformer | Action, Condition, Transformer |
Invalid Connections
- Triggers cannot be targets - Nothing can connect TO a trigger
- Circular connections - A node cannot eventually connect back to itself
- Type mismatches - Some nodes require specific input types
Best Practices
Node Configuration
- Use expressions - Make configs dynamic with
{{path.to.value}} - Validate early - Check required data at the start of workflow
- Handle errors - Configure retry settings for unreliable operations
- Log important data - Use Set Variable to track state
Workflow Design
- Start with triggers - Always have a clear entry point
- Validate data - Use conditions to handle edge cases
- Keep it simple - Break complex workflows into smaller ones
- Document - Use node labels to explain purpose
Next Steps
- Execution Engine - How nodes are executed
- Extending Workflows - Create custom nodes
- Examples - See nodes in action