saltcorn-workflow-kit
saltcorn-workflow-kit
Advanced workflow nodes for Saltcorn — AI integrations, logic controls, integrations, and messaging in one plugin.
23 nodes across 4 categories:
| Category | Nodes |
|---|---|
| AI | Anthropic Chat, OpenAI Chat, Gemini Chat, AI Router |
| Logic | Human Approval, Check Approval Status, Retry Counter, Scheduled Delay, Set Variable, If/Else Branch, Handlebars Template, JSON Transform, Loop Iterator, Debug Log |
| Integration | HTTP Request, DB Query, Trigger Workflow, Google Sheets, Airtable |
| Messaging | Twilio SMS, Slack Message, Email (SMTP/SendGrid), WhatsApp Message |
Installation
# From your Saltcorn root
npm install saltcorn-workflow-kit
Then in Saltcorn admin → Plugins → Install from npm → saltcorn-workflow-kit.
Or for local development:
cd saltcorn-workflow-kit
npm install
# In Saltcorn admin → Plugins → Install local plugin → point to this directory
Common Patterns
{{ }} interpolation
Most text fields support {{ variable_name }} substitution from the current workflow context:
Hello {{ first_name }}, your order {{ order_id }} is ready.
Nested paths work too: {{ user.email }}, {{ address.city }}.
Workflow context (ctx.row)
Every step receives the full workflow context as ctx.row. Each step's return object is merged into the context — later steps can read variables set by earlier steps.
AI Nodes
wfkit_anthropic_chat — Anthropic Chat
Send a prompt to Claude (Anthropic) and store the response as a workflow variable.
| Field | Type | Description |
|---|---|---|
api_key |
String | Anthropic API key (sk-ant-...). Supports {{ }}. |
model |
Select | Model to use. Default: claude-haiku-4-5-20251001. |
system_prompt |
Textarea | Optional system/persona instructions. {{ }} supported. |
user_message |
Textarea | The user message to send. {{ }} supported. Required. |
max_tokens |
Integer | Max tokens in the response. Default: 1024. |
temperature |
Float | 0–1, creativity. Default: 0.7. |
output_var |
String | Variable to store the text response. Default: ai_response. |
Returns: { [output_var], ai_response, ai_model, ai_input_tokens, ai_output_tokens }
Example next_step expression:
ai_response.includes("urgent") ? "step_escalate" : "step_continue"
wfkit_openai_chat — OpenAI Chat
Send a prompt to OpenAI (GPT-4o, GPT-4, etc.) and store the response.
| Field | Type | Description |
|---|---|---|
api_key |
String | OpenAI API key (sk-...). |
model |
Select | Model. Default: gpt-4o-mini. |
system_prompt |
Textarea | System instructions. {{ }} supported. |
user_message |
Textarea | User message. {{ }} supported. Required. |
max_tokens |
Integer | Max response tokens. Default: 1024. |
temperature |
Float | 0–2. Default: 0.7. |
output_var |
String | Variable for response text. Default: ai_response. |
Returns: { [output_var], ai_response, ai_model, ai_finish_reason, ai_input_tokens, ai_output_tokens }
wfkit_gemini_chat — Gemini Chat
Send a prompt to Google Gemini (via Generative Language API).
| Field | Type | Description |
|---|---|---|
api_key |
String | Google AI Studio API key. |
model |
Select | Model. Default: gemini-1.5-flash. |
system_prompt |
Textarea | System instructions. {{ }} supported. |
user_message |
Textarea | User message. {{ }} supported. Required. |
max_tokens |
Integer | Max output tokens. Default: 1024. |
temperature |
Float | 0–2. Default: 0.7. |
output_var |
String | Variable for response. Default: ai_response. |
Returns: { [output_var], ai_response, ai_model, ai_finish_reason }
wfkit_ai_router — AI Router
Route workflow execution based on AI classification of input text. The AI labels the input according to your defined categories, then sets a branch variable you can use in next_step.
| Field | Type | Description |
|---|---|---|
provider |
Select | anthropic, openai, or gemini. |
api_key |
String | API key for the chosen provider. |
model |
String | Model name. Defaults to a fast model per provider. |
input_text |
String | Text to classify. {{ }} supported. |
categories |
Textarea | JSON array of category objects: [{"name":"urgent","description":"..."},...] |
branch_var |
String | Variable set to the matched category name. Default: ai_route. |
confidence_var |
String | Variable set to confidence 0–1. Default: ai_confidence. |
fallback_category |
String | Category to use if classification fails. Default: unknown. |
Returns: { [branch_var], [confidence_var], ai_route, ai_confidence, ai_reasoning }
Example next_step:
ai_route === "billing" ? "step_billing" : ai_route === "support" ? "step_support" : "step_general"
Logic Nodes
wfkit_human_approval — Human Approval
Pause a workflow pending a human decision. Sends approval/reject links via email. The decision fires a wfkit_approval_decided event for downstream triggers.
| Field | Type | Description |
|---|---|---|
to_emails |
String | Comma-separated approver emails. {{ }} supported. |
label |
String | Short description shown in the email subject. {{ }} supported. |
details |
Textarea | Optional body text. {{ }} supported. |
base_url |
String | Your Saltcorn app's public URL (e.g. https://app.example.com). |
expires_hours |
Integer | Link validity in hours. Default: 72. |
channel |
String | Optional event channel for filtering downstream. |
context_fields |
String | Comma-separated field names to snapshot into the approval record. |
token_var |
String | Variable to store the approval token. Default: approval_token. |
Returns: { [token_var], approval_token, approval_status, approve_url, reject_url, status_url, created_at }
HTTP routes registered (GET, no CSRF):
/wfkit/approval/:token/approve— HTML confirmation page, fires event/wfkit/approval/:token/reject— HTML rejection page, fires event/wfkit/approval/:token/status— JSON status check
Requires SMTP configured in Saltcorn admin → Settings → Email.
wfkit_approval_status — Check Approval Status
Poll the current status of a pending approval by token. Returns the decision for use in next_step expressions.
| Field | Type | Description |
|---|---|---|
token |
String | Approval token (e.g. {{ approval_token }}). Required. |
decision_var |
String | Variable to store decision: approved, rejected, pending, expired. Default: approval_decision. |
Returns: { [decision_var], approval_decision, status, decided_at, decided_by }
Typical pattern: poll this node every N minutes, use next_step:
approval_decision === "approved" ? "step_execute" :
approval_decision === "rejected" ? "step_cancel" : "step_poll"
wfkit_retry_counter — Retry Counter
Track how many times a workflow step has been retried. Use with next_step to loop back or give up after N attempts.
| Field | Type | Description |
|---|---|---|
counter_var |
String | Variable that holds the count. Default: retry_count. |
max_retries |
Integer | Maximum allowed retries before exhausted. Default: 3. |
reset |
Bool | Reset the counter to 0 on this execution. Default: false. |
exhausted_var |
String | Boolean variable set to true when max is reached. Default: retry_exhausted. |
Returns:
{
[counter_var], // incremented count (Integer)
[exhausted_var], // true when count >= max_retries
retry_count, // same as [counter_var]
retry_exhausted, // same as [exhausted_var]
attempts_remaining, // max_retries - count
max_retries
}
next_step pattern (loop with give-up):
retry_exhausted ? "step_fail" : "step_attempt"
wfkit_delay — Scheduled Delay
Pause workflow execution for a specified duration.
- ≤ 5 minutes: sleeps synchronously (occupies a worker).
- > 5 minutes: sets
wake_atand returnsdelay_status: "deferred"— the workflow engine must implement its own scheduling to re-invoke this step atwake_at.
| Field | Type | Description |
|---|---|---|
amount |
Integer | How long to wait. Required. |
unit |
Select | ms, seconds, minutes, hours, days. Default: seconds. |
label |
String | Optional log label. |
Returns:
{
delay_status, // "completed" or "deferred"
delay_ms, // actual ms waited / requested
wake_at, // ISO timestamp (deferred mode only)
delay_unit,
delay_amount
}
wfkit_set_variable — Set Variable
Set one or more workflow variables with typed assignment. Supports literal values, expressions, and cross-variable references.
| Field | Type | Description |
|---|---|---|
assignments |
Textarea | JSON array of { name, value, type } objects. |
Assignment types:
| Type | Behaviour | Example value |
|---|---|---|
string |
{{ }} interpolated |
"Hello {{ name }}" |
number |
Number(value) |
"42" |
bool |
truthy strings (true, 1, yes) → true |
"true" |
expr |
JavaScript expression, row in scope |
"amount * 1.2" |
Example assignments JSON:
[
{ "name": "full_name", "value": "{{ first_name }} {{ last_name }}", "type": "string" },
{ "name": "tax", "value": "amount * 0.18", "type": "expr" },
{ "name": "is_large_order","value": "amount > 1000", "type": "expr" }
]
Returns: one key per assignment name, plus set_variable_count.
wfkit_if_else — If/Else Branch
Evaluate a JavaScript condition against the workflow context and set a branch variable. Supports else-if chains.
| Field | Type | Description |
|---|---|---|
condition |
String | JS expression. All context variables are in scope. |
branch_var |
String | Variable set to the matched branch value. Default: branch. |
value_if_true |
String | Value for branch_var when condition is true. Default: "true". |
value_if_false |
String | Value when no condition matches. Default: "false". |
else_if |
Textarea | JSON array of { condition, value } objects to check in order. |
Returns: { [branch_var], branch, branch_matched, condition_result }
Example:
condition: amount > 1000
value_if_true: high
value_if_false: low
Else-if chains:
[
{ "condition": "status === 'vip'", "value": "vip_path" },
{ "condition": "status === 'member'","value": "member_path" }
]
next_step using branch:
branch === "high" ? "step_review" : "step_auto_approve"
wfkit_template — Handlebars Template
Render a Handlebars template against the workflow context and store the result.
| Field | Type | Description |
|---|---|---|
template |
Textarea | Handlebars template string. Required. |
output_var |
String | Variable to store the rendered string. Default: template_output. |
partials |
Textarea | JSON object { "name": "template string" } for {{> name}}. |
strict |
Bool | Throw on undefined variables instead of rendering empty. Default: false. |
Built-in helpers:
| Helper | Example | Output |
|---|---|---|
{{upper str}} |
{{upper name}} |
ALICE |
{{lower str}} |
{{lower name}} |
alice |
{{trim str}} |
{{trim " hi "}} |
hi |
{{default val fallback}} |
{{default nickname "friend"}} |
value or friend |
{{json val}} |
{{json items}} |
["a","b"] |
{{currency val}} |
{{currency amount}} |
1,234.56 |
{{date val}} |
{{date created_at}} |
locale date string |
{{#if (eq a b)}} |
{{#if (eq status "open")}} |
conditional |
{{#if (gt a b)}} |
{{#if (gt amount 100)}} |
numeric compare |
{{#if (and a b)}} |
logical and/or |
Returns: { [output_var], template_output }
Example template:
Dear {{upper name}},
Your order #{{order_id}} ({{currency amount}}) is {{status}}.
{{#if (gt amount 500)}}
Your VIP discount has been applied.
{{/if}}
wfkit_json_transform — JSON Transform
Query and transform JSON data using JSONata expressions.
| Field | Type | Description |
|---|---|---|
expression |
String | JSONata expression. Required. |
input_var |
String | Variable to use as expression root. If blank, full ctx.row is the input. |
output_var |
String | Variable to store the result. Default: transform_result. |
bindings |
Textarea | JSON object of extra variables bound into the expression. |
on_empty |
Select | What to do when result is null/undefined: null, empty_array, empty_string, error. Default: null. |
Behaviour of input_var:
- With
input_var: JSONata operates onrow[input_var]directly. Use bare expressions:[status="open"]to filter an array. - Without
input_var: JSONata operates on the fullrow. Write expressions likeorders[status="open"]to reference a named variable.
Returns: { [output_var], transform_result, transform_type, transform_count }
Examples:
Filter open orders (full row as input, no input_var):
orders[status = "open"].{ "id": id, "total": amount }
Sum order amounts:
$sum(orders.amount)
Reshape:
items.{ "label": name, "price": "$" & $string(price) }
wfkit_loop — Loop Iterator
Iterate over an array one item per workflow pass. On each pass returns one item; set next_step to jump back to the loop until loop_done is true.
Modes:
| Mode | Description |
|---|---|
cursor |
Returns one item per pass via an index variable. |
collect |
Filter an array with a JSONata expression, return the whole filtered array at once. |
map |
Transform each item with a JSONata expression, return the new array at once. |
reduce |
Aggregate: sum, min, max, concat, first, last. |
| Field | Type | Description |
|---|---|---|
array_var |
String | Variable that holds the array to iterate. Required. |
mode |
Select | cursor, collect, map, reduce. Default: cursor. |
item_var |
String | Variable for the current item (cursor mode). Default: loop_item. |
index_var |
String | Variable for the current index (cursor mode). Default: loop_index. |
expression |
String | JSONata for collect/map/reduce modes. |
reduce_op |
Select | sum, min, max, concat, first, last (reduce mode). |
output_var |
String | Output variable for collect/map/reduce. Default: loop_result. |
Cursor mode returns:
{
loop_item, // current item (null when done)
loop_index, // current 0-based index
loop_done, // true when exhausted
loop_length, // array.length
[item_var], // alias for loop_item
[index_var], // alias for loop_index
}
Cursor loop pattern:
- Step A (
wfkit_loop, mode=cursor): setsloop_item,loop_done - Step B: process
loop_item - Step C:
next_step=loop_done ? "step_finish" : "step_a"
Collect/map/reduce return { [output_var], loop_result } in a single pass.
wfkit_debug_log — Debug Log
Dump workflow context to the server log for debugging. Never throws — safe to leave in place.
| Field | Type | Description |
|---|---|---|
label |
String | Log label for grep. Default: wfkit:debug. |
mode |
Select | all (every variable), selected (named variables), diff (changed since last checkpoint). |
variables |
String | Comma-separated names (selected mode only). |
checkpoint_var |
String | Snapshot variable (diff mode). Default: debug_checkpoint. |
max_depth |
Integer | Truncate values longer than this many chars. Default: 500. |
enabled |
Bool | Set false to silence without removing. Default: true. |
Returns: {} (pass-through), except diff mode which returns { [checkpoint_var] }.
Log format (structured JSON, visible in saltcorn serve output):
{
"label": "after_approval",
"mode": "all",
"step_count": 5,
"variables": { "name": "\"Alice\"", "amount": "1200" }
}
Integration Nodes
wfkit_http_request — HTTP Request
Make an HTTP/HTTPS request to any external API.
| Field | Type | Description |
|---|---|---|
url |
String | Request URL. {{ }} supported. Required. |
method |
Select | GET, POST, PUT, PATCH, DELETE. Default: GET. |
headers |
Textarea | JSON object of request headers. {{ }} supported. |
body |
Textarea | Request body (JSON or text). {{ }} supported. |
body_type |
Select | json, text, form. Default: json. |
auth_type |
Select | none, bearer, basic. |
auth_token |
String | Token (bearer) or user:pass (basic). {{ }} supported. |
timeout_ms |
Integer | Request timeout in ms. Default: 10000. |
output_var |
String | Variable for response body. Default: http_response. |
fail_on_error |
Bool | Throw if status >= 400. Default: true. |
Returns: { [output_var], http_response, http_status, http_ok, http_headers }
Example: call a REST API and get a JSON body:
url: https://api.example.com/orders/{{ order_id }}
method: GET
auth_type: bearer
auth_token: {{ api_key }}
wfkit_db_query — DB Query
Query any Saltcorn table. Returns rows, a single row, a count, or an aggregate.
| Field | Type | Description |
|---|---|---|
table_name |
String | Saltcorn table name. Required. |
operation |
Select | getRows, getRow, countRows, aggregate. |
where |
Textarea | JSON object of filter conditions. {{ }} in values. |
order_by |
String | Column to sort by. |
order_desc |
Bool | Descending order. Default: false. |
limit |
Integer | Max rows to return. |
offset |
Integer | Skip N rows. |
aggregate_field |
String | Field to aggregate (sum, min, max, avg). |
aggregate_fn |
Select | sum, min, max, avg, count. |
output_var |
String | Variable for results. Default: db_result. |
Where clause operators:
{ "status": "open" } // equals
{ "amount": { "gt": 100 } } // greater than
{ "amount": { "gte": 100, "lt": 1000 } } // range
{ "tag": ["featured", "sale"] } // IN array
{ "deleted_at": null } // IS NULL
Returns:
getRows→{ [output_var], db_result, db_count }getRow→{ [output_var], db_result, db_found }countRows→{ db_count, [output_var] }aggregate→{ db_aggregate, [output_var] }
wfkit_trigger_workflow — Trigger Workflow
Launch another Saltcorn workflow (trigger) from within a workflow.
| Field | Type | Description |
|---|---|---|
trigger_name |
String | Name of the Saltcorn trigger to fire. {{ }} supported. Required. |
mode |
Select | fire-and-forget (emitEvent), direct-async (no await), direct-sync (await result). Default: fire-and-forget. |
payload |
Textarea | JSON object merged as the trigger's row. {{ }} supported. |
extra_fields |
Textarea | key=value lines (one per line) added to the payload. {{ }} supported. |
event_type |
String | Event type for fire-and-forget mode. Default: trigger name. |
channel |
String | Event channel for fire-and-forget. |
output_var |
String | Variable for sync mode result. Default: triggered_result. |
Modes:
fire-and-forget: emits a Saltcorn event; fastest, no result.direct-async: calls trigger without awaiting; no result.direct-sync: calls trigger and waits; result available inoutput_var.
Returns: { trigger_fired, trigger_name, trigger_mode, [output_var] }
wfkit_google_sheets — Google Sheets
Read and write Google Sheets using a Service Account (no OAuth flow needed).
Setup:
- Google Cloud Console → create a Service Account → create JSON key.
- Share the sheet with the service account email.
- Paste the JSON key content into the
service_account_jsonfield.
| Field | Type | Description |
|---|---|---|
service_account_json |
Textarea | Full service account JSON key. Required. |
spreadsheet_id |
String | From the sheet URL: .../spreadsheets/d/{ID}/edit. {{ }} supported. |
range |
String | A1 notation: Sheet1!A1:D100. {{ }} supported. |
operation |
Select | readRange, readRows, appendRow, updateRange, clearRange. |
row_values |
Textarea | JSON array for write ops: ["A","B","C"] or [["A","B"],["C","D"]]. |
value_input_option |
Select | USER_ENTERED (parse dates/formulas) or RAW. Default: USER_ENTERED. |
output_var |
String | Variable for read results. Default: sheets_result. |
Operations:
| Op | Description | Returns |
|---|---|---|
readRange |
Raw 2D array | [[row1col1,...],...] |
readRows |
Rows as objects (row 1 = headers) | [{ Name:"Alice", Age:"30" },...] |
appendRow |
Append one or more rows | sheets_appended_rows, sheets_updated_range |
updateRange |
Overwrite a range | sheets_updated_rows, sheets_updated_range |
clearRange |
Clear cells | sheets_cleared, sheets_range |
wfkit_airtable — Airtable
Read and write Airtable records via the Airtable Web API.
Setup: Create a Personal Access Token at airtable.com/create/tokens with data.records:read and/or data.records:write scopes.
| Field | Type | Description |
|---|---|---|
api_token |
String | Personal access token. Required. |
base_id |
String | Base ID (starts with app). {{ }} supported. |
table_name |
String | Table name or ID. {{ }} supported. |
operation |
Select | listRecords, getRecord, createRecord, updateRecord, deleteRecord. |
filter_formula |
String | Airtable formula: AND({Status}='open',{Amt}>100). {{ }} supported. |
sort_field |
String | Sort by field name. |
sort_direction |
Select | asc or desc. |
max_records |
Integer | Max records (listRecords). Default: 100. |
view |
String | Restrict to a named view. |
record_id |
String | Record ID (starts with rec). {{ }} supported. |
fields |
Textarea | JSON fields for create/update: { "Name": "{{ name }}" }. |
output_var |
String | Variable for results. Default: airtable_result. |
Records are returned as flat objects: { id: "rec...", Name: "Alice", Status: "open" }
Returns (listRecords): { [output_var], airtable_result, airtable_count, airtable_raw }
Messaging Nodes
wfkit_twilio_sms — Twilio SMS
Send an SMS via the Twilio REST API.
| Field | Type | Description |
|---|---|---|
account_sid |
String | Twilio Account SID (AC...). |
auth_token |
String | Twilio Auth Token. |
from |
String | Sender number in E.164 format or a Messaging Service SID. |
to |
String | Recipient number in E.164 format. {{ }} supported. |
body |
Textarea | Message text (max 1600 chars). {{ }} supported. |
output_var |
String | Variable for message SID. Default: sms_sid. |
Returns: { [output_var], sms_sid, sms_status, sms_to, sms_from }
wfkit_slack_message — Slack Message
Post a message to a Slack channel or user via Incoming Webhook or Bot Token.
| Field | Type | Description |
|---|---|---|
webhook_url |
String | Incoming Webhook URL (https://hooks.slack.com/...). |
bot_token |
String | Bot OAuth token (xoxb-...) — alternative to webhook. |
channel |
String | Channel (#general) or user ID (@U...). Required for bot token mode. {{ }} supported. |
text |
Textarea | Message text. Slack mrkdwn supported. {{ }} supported. |
blocks |
Textarea | JSON Block Kit array. {{ }} supported. |
username |
String | Display name override. |
icon_emoji |
String | Emoji icon (:robot_face:). |
thread_ts |
String | Reply in thread: pass the parent message timestamp. {{ }} supported. |
output_var |
String | Variable for response. Default: slack_response. |
Returns: { [output_var], slack_ok, slack_ts, slack_channel }
wfkit_email — Email (SMTP/SendGrid)
Send an email via Saltcorn's built-in SMTP, custom SMTP, or SendGrid.
| Field | Type | Description |
|---|---|---|
transport |
Select | saltcorn (use configured SMTP), smtp (custom), sendgrid. |
to |
String | Recipient(s), comma-separated. {{ }} supported. Required. |
from |
String | Sender address. {{ }} supported. |
subject |
String | Email subject. {{ }} supported. |
body |
Textarea | Email body (HTML or plain text). {{ }} supported. |
cc |
String | CC addresses. {{ }} supported. |
bcc |
String | BCC addresses. {{ }} supported. |
smtp_host |
String | SMTP host (smtp transport). |
smtp_port |
Integer | SMTP port. Default: 587. |
smtp_user |
String | SMTP username. |
smtp_pass |
String | SMTP password. |
smtp_secure |
Bool | TLS. Default: false. |
sendgrid_api_key |
String | SendGrid API key (sendgrid transport). |
output_var |
String | Variable for result. Default: email_result. |
Returns: { [output_var], email_sent, email_to, email_message_id }
wfkit_whatsapp_message — WhatsApp Message
Send a WhatsApp message via the Meta Cloud API (Graph API v18+).
Setup:
- Create a Meta App at developers.facebook.com → add WhatsApp product.
- Generate a System User access token.
- Get your Phone Number ID (numeric ID, not the phone number).
| Field | Type | Description |
|---|---|---|
access_token |
String | Meta system user access token. |
phone_number_id |
String | Numeric Phone Number ID from Meta Developer Console. |
to |
String | Recipient phone in E.164 format. {{ }} supported. |
message_type |
Select | text, template, interactive. |
text_body |
Textarea | Message text (text type). WhatsApp markdown: *bold*, _italic_. {{ }} supported. |
preview_url |
Bool | Enable link previews in text messages. |
template_name |
String | Approved template name (template type). |
template_language |
String | Language code: en_US, en. Default: en_US. |
template_components |
Textarea | JSON array of variable substitutions for the template. |
interactive_json |
Textarea | Full interactive object per Meta API spec. |
output_var |
String | Variable for message ID. Default: whatsapp_message_id. |
Returns: { [output_var], whatsapp_message_id, whatsapp_to, whatsapp_type, whatsapp_wa_id }
Template components example:
[
{
"type": "body",
"parameters": [
{ "type": "text", "text": "{{ customer_name }}" },
{ "type": "text", "text": "{{ order_id }}" }
]
}
]
Workflow Recipes
Retry with exponential back-off
Step 1 wfkit_retry_counter counter_var=attempt, max_retries=3
Step 2 wfkit_http_request url=https://api.example.com/...
[on error → Step 1]
Step 3 [on success → continue]
next_step: retry_exhausted ? "step_fail" : "step_1"
Human-in-the-loop
Step 1 wfkit_human_approval to_emails={{ manager_email }}, label=Order {{ order_id }}
Step 2 wfkit_delay amount=5, unit=minutes (poll interval)
Step 3 wfkit_approval_status token={{ approval_token }}
next_step: approval_decision === "approved" ? "step_proceed"
: approval_decision === "rejected" ? "step_cancel"
: "step_2" ← keeps polling
Step 4 wfkit_email (proceed confirmation)
AI classification → branch
Step 1 wfkit_ai_router input_text={{ user_message }},
categories=[{"name":"billing"},{"name":"support"},{"name":"sales"}]
next_step: ai_route === "billing" ? "step_billing"
: ai_route === "support" ? "step_support"
: "step_sales"
Loop over DB rows
Step 1 wfkit_db_query table=orders, operation=getRows,
where={"status":"pending"}, output_var=orders
Step 2 wfkit_loop array_var=orders, mode=cursor, item_var=order
Step 3 wfkit_email to={{ order.email }}, subject=Your order {{ order.id }}
next_step: loop_done ? "step_done" : "step_2"
Render email body with template
Step 1 wfkit_template template=<template content>, output_var=email_body
Step 2 wfkit_email body={{ email_body }}, to={{ customer_email }}
Custom Event Types
The plugin registers one custom Saltcorn event type:
| Event | Description |
|---|---|
wfkit_approval_decided |
Fired when an approval link is clicked. Payload: { token, decision, context, label }. Use channel for filtering. |
Environment / Credentials
| Integration | Credential location |
|---|---|
| Anthropic, OpenAI, Gemini | API key field per node (or {{ credentials.xxx }}) |
| Twilio | account_sid + auth_token fields |
| Slack | Webhook URL or xoxb- bot token field |
| WhatsApp (Meta) | access_token + phone_number_id fields |
| Google Sheets | Service Account JSON key pasted into node config |
| Airtable | Personal Access Token field |
| Email (saltcorn transport) | Saltcorn Admin → Settings → Email (SMTP) |
| Email (custom SMTP) | smtp_* fields per node |
| Email (SendGrid) | sendgrid_api_key field per node |
All credential fields support {{ }} interpolation, so you can store secrets as Saltcorn credentials and reference them as {{ credentials.my_key }}.