BorgIQ Builder
Build Actors and Triggers that power BorgIQ automation workflows.
Table of Contents
BorgIQ Platform Overview
BorgIQ is an automation platform where nodes are called Actors. Workflows chain Actors together with connections (edges). Each actor emits messages stored under msg.ActorName.
Workspaces and Canvases
BorgIQ organizes workflows in a hierarchical structure:
| Concept |
Description |
Identifier |
| Workspace |
A container for canvases, connections, and team members. Workspaces provide isolation and access control. |
workspaceSlug (e.g., my-team, prod-ws) |
| Canvas |
A container for one or more workflows and their connections. Each workflow within a canvas has its own trigger actor. |
canvasSlug (e.g., process-orders, send-notifications) |
| Actor |
An individual node within a canvas that performs work or triggers execution. |
actorId (e.g., ACTR01kd6tesvky0mh8x1css3sv5yg) |
Slug Format:
- Workspace slugs: 5-10 lowercase alphanumeric characters with hyphens (e.g.,
john-dev)
- Canvas slugs: 2-255 lowercase alphanumeric characters with hyphens (e.g.,
skill-test)
Cross-Canvas/Workspace Calls:
Actors can invoke sub-flows in other canvases or workspaces using CallFlowActor. When workspaceSlug or canvasSlug is omitted, the current workspace/canvas is assumed.
Actor Categories
There are two categories of actors:
| Category |
Description |
Examples |
| Trigger Actors |
Start workflows (flowruns). Each workflow has exactly one trigger. A canvas can contain multiple workflows, each with its own trigger. |
ButtonTriggerActor, WebhookTriggerActor, ScheduledTriggerActor |
| Task Actors |
Perform work within the workflow. Process data, make API calls, route messages, etc. |
HttpRequestActor, DenoActor, AiActor, RouterActor |
Example workflow: TriggerActor -> TaskActor1 -> TaskActor2 produces:
{
"msg": {
"trigger_actor": { "...output from trigger..." },
"task_actor_1": { "...output from task 1..." },
"task_actor_2": { "...output from task 2..." }
}
}
Concurrent Execution Model
BorgIQ executes all downstream actors concurrently by default. When an actor emits a message, all connected downstream actors start executing in parallel without any explicit configuration.
A
/ \
B C <- B and C run concurrently when A emits
\ /
D <- D receives TWO separate messages (one from B, one from C)
Key behavior:
- If A connects to both B and C, both B and C execute concurrently when A emits
- D will receive two separate messages and execute twice (once for B's output, once for C's output)
- No
fork action is needed for parallel execution—it happens automatically
When to use fork/forkJoin:
Only use the fork and forkJoin MessageProcessorActor actions when you need to synchronize parallel paths and emit a single combined message. See message-processor-actor.md for detailed documentation, workflow-patterns.md for complete examples, and fork-join-common-mistakes.md for common pitfalls to avoid.
| Scenario |
Use Fork/ForkJoin? |
| Run B and C in parallel, D processes each separately |
No - just connect A→B→D and A→C→D |
| Run B and C in parallel, D needs combined results from both |
Yes - use fork before split, forkJoin to recombine |
| Fire-and-forget parallel notifications (email + Slack) |
No - just connect to both actors |
| Parallel API calls where you need all results together |
Yes - use fork/forkJoin pattern |
Critical rules:
forkJoin requires enableSTM: true
- MessageProcessorActor always uses only
SPRTdefault (fork uses multiple edges, not multiple sourcePorts)
- Only RouterActor, AiRouterActor, InterfaceActor, AiAgentActor, and AgentHarnessActor use multiple sourcePorts
Routing to Specialized Skills
This skill is the hub of the borgiq-builder plugin. It covers actor wiring, edges, msgVars, expressions, and overall workflow composition. Four spoke skills ship in the same plugin and load automatically when their domain appears in the user's request. Pull them in actively when the work crosses into their area — they're more opinionated and focused than this hub.
| Spoke |
Load when the user is doing… |
What it owns |
borgiq-form-builder |
Interface pages, forms, signups, surveys, approval forms, data-entry UIs, web-viewer embeds |
InterfaceTriggerActor + InterfaceActor + form components + themes + webViewer styling |
borgiq-react-app-builder |
Custom app UIs — dashboards, data explorers, SPAs, any component-based or multi-file frontend, npm UI libraries, useEndpoint, useGetSession (who is viewing the app), useStreamTail (follow a workspace stream live); also owns maintaining legacy raw-HTML AppTriggerActor apps |
ReactAppTriggerActor + server-side Vite build + codeDir/options.files model + @borgiq/actors SDK + webhook endpoints + declared stream tails + viewer session + the app theme library |
borgiq-agent-builder |
Autonomous AI behavior — AiAgentActor (serverless coding agent with filesystem/bash + tools), AgentHarnessActor (sandboxed Claude Code), McpServerActor (expose tools to external agents) |
AiActor when-not-to-use + AiAgentActor + AgentHarnessActor + McpServerActor |
borgiq-json-schema-builder |
Non-trivial JSON schemas — AiActor outputSchema, agent tool input schemas, Collection item schemas, Callable response schemas |
All schema-design decisions, anti-patterns, and the BorgIQ type: any convention |
Cross-domain example: "build a flow with an interface form that takes a customer name, hands it to an AI agent that researches them, posts the result to Slack" → hub orchestrates + borgiq-form-builder designs the form + borgiq-agent-builder designs the agent + borgiq-json-schema-builder defines the research output schema. The hub stays in context throughout and handles wiring, edges, IDs, and the Slack HTTP actor.
TypeScript Definitions
Complete TypeScript/Zod schema definitions for all actors are available in references/typescript/. Use these to understand exact data structures, validation rules, and type constraints.
| Reference File |
Description |
| actor-schemas-triggers.md |
Trigger actor options and results (Button, Webhook, Email, Interface, App, Scheduled, Universal, Callable) |
| actor-schemas-task-core.md |
Core task actor schemas (AiActor, AiAgentActor, DenoActor, PythonActor, RouterActor, etc.) |
| actor-schemas-task-http.md |
HttpRequestActor options and authentication types |
| actor-schemas-task-datastore.md |
DataStoreActor actions (legacy — kept for TypeScript type reference) |
| actor-schemas-task-collection.md |
CollectionActor actions (query, getItem, putItem, batchGet, batchWrite, etc.) |
| actor-schemas-task-stream.md |
StreamActor actions (createStream, appendData, readStream, getStreamInfo, etc.) |
| actor-schemas-task-messageprocessor.md |
MessageProcessorActor actions (inject, split, collect, fork, forkJoin, delay, etc.) |
| actor-schemas-comment.md |
CommentActor schema |
| form-components.md |
Interface form component schemas (InterfaceTriggerActor and InterfaceActor only, not used by AppTriggerActor) |
| schemas.md |
Common schemas (IDs, files, runtime types, context, signals) |
| common-types.md |
Shared types, AI model definitions, canvas, runtime, sandbox types |
Usage: When building actors or understanding output structures, read the relevant TypeScript reference markdown file to find exact field names, types, and validation rules. Each file contains a table of contents linking to individual type definitions.
Task Actor Types
| Type |
Description |
Reference |
| HttpRequestActor |
Makes REST API calls to external services (Gmail, GitHub, Airtable, etc.) |
http-request-actor.md |
| DenoActor |
Executes custom TypeScript/JavaScript code in a sandboxed Deno runtime |
deno-actor.md |
| PythonActor |
Executes custom Python code in a sandboxed Python runtime with UV package management |
python-actor.md |
| AiActor |
Invokes AI models (LLMs) for text generation, structured output, and AI-powered tasks |
ai-actor.md |
| AiAgentActor |
Autonomous AI coding agent running in checkpointed serverless segments. Has a private workspace with built-in filesystem/bash tools (read/write/edit/bash/grep/find/ls, plus an opt-in deno tool for running code) plus BorgIQ actors as tools, session continuation via sessionId, and workspace zip in/out (volumeZipFile → outputZipFile). Has two output ports: Done (final result + zips) and Status (assistant turns + tool results). Tool actors are rendered inside the agent boundary with empty edges. |
ai-agent-actor.md |
| DeprecatedAiAgent |
Legacy orchestrator-loop AI agent (pre-2026 AiAgentActor) — no filesystem or sessions. Hidden from the palette; existing instances keep running. Do not create new instances — use AiAgentActor. |
deprecated-ai-agent.md |
| AgentHarnessActor |
Runs Claude Code in an isolated sandbox (E2B or Daytona) with full filesystem access, code execution, session persistence via sessionId, and queued inbound messages. Supports volumeZipFile for context, network control, MCP servers, environment variables, and returns workspace + session data zips. Use when the agent needs to execute code, install packages, or persist state across sessions. Has two output ports: Done (final result with output files) and Status (real-time execution updates). |
agent-harness-actor.md |
| AiRouterActor |
Routes messages to different outputs based on AI-powered classification |
ai-router-actor.md |
| RouterActor |
Routes messages based on boolean conditions (if/else, switch logic) |
router-actor.md |
| MessageProcessorActor |
Processes, transforms, and controls message flow (inject data, delay, split/collect arrays, dedupe, filter, fork/forkJoin, callbacks). Important: Always has only SPRTdefault sourcePort—fork uses multiple edges to create parallel paths, not multiple sourcePorts. |
message-processor-actor.md |
| WebhookResponseActor |
Sends custom HTTP responses back to WebhookTriggerActor callers |
webhook-response-actor.md |
| CallableResponseActor |
Returns data from sub-flows back to parent flows (CallFlowActor). Only valid in flows triggered by CallableTriggerActor. |
callable-response-actor.md |
| CallFlowActor |
Invokes sub-flows by calling a CallableTriggerActor in another canvas/workspace |
call-flow-actor.md |
| InterfaceActor |
Renders a web form/page mid-workflow with two output ports: Meta (URL info on render) and Event (form submission data) |
interface-actor.md |
| SendEmailActor |
Sends text/HTML emails with optional attachments |
send-email-actor.md |
| CollectionActor |
Persistent structured storage organized into named collections with labels, TTL, queries, batch operations, and transactions. Recommended for all new storage needs. One collection per app — model all entity types with key prefixes (single-collection design). |
collection-actor.md |
| StreamActor |
Append-only, ordered, cursor-addressed record logs — event ingestion, audit trails, activity feeds, and incremental processing that resumes from a persisted cursor. Reads return one bounded page, never the stream. Streams must be created before use and expire one hour after the last append unless created persistent: true or with an explicit idleTtlSeconds. Use for "what happened, in order"; use CollectionActor for "the current value of X" (Collections vs Streams). |
stream-actor.md |
| McpServerActor |
Exposes its child tool actors as an MCP (Model Context Protocol) server endpoint that external AI agents (Claude Desktop, Cursor, custom agents) can connect to. Reuses the AiAgentActor tool-actor pattern (aiAgentToolActorIds, ${{aiInput}} schema filtering) — the difference is that an external MCP client drives tool invocations instead of an internal LLM loop. |
mcp-server-actor.md |
| CommentActor |
Non-functional UI element for adding notes, TODOs, and documentation to workflows |
comment-actor.md |
⚠️ Before hand-building an integration actor — especially an HttpRequestActor — search the template catalog first. BorgIQ ships vetted templates for most third-party actions (Gmail, Slack, GitHub, Google, Notion, …). Adapting a template is the single biggest defense against the most common failure mode: hand-writing an actor's options, sourcePorts, and schemas from scratch and getting them subtly wrong. Only hand-build when no template fits.
borgiq templates apps --search "<vendor>" --json # run a few queries, e.g. gmail, google, slack
borgiq templates list --app-id TAPP... --json # list that app's templates (paginates/sorts)
borgiq templates get ATMP... --json \ # fetch the chosen template, then convert it:
| borgiq scaffold actor-from-template --output actor.json --print-id
borgiq canvas-actors create <canvasSlugOrId> <actorId> --file actor.json --json
In a bundle, write the templates get actor payload (already ExportedCanvasActor object shape) as actor.yaml, apply the template fixups (fresh actor ID and trigger keys, keep template provenance), and complete the three-edit rule. scaffold actor-from-template produces the YAML-string CanvasActor mutation shape for the direct/batch fallback and performs those fixups automatically. Full flow: Deploying and Testing with the CLI.
Choosing a Task Actor Type
Important: DenoActor vs MessageProcessorActor
Use MessageProcessorActor as the default for data transformations. It handles most transformation needs via YAML configuration and ${{ }} expressions without custom code.
Use DenoActor (or PythonActor) ONLY when you need:
- Fetch/HTTP requests - Making API calls within custom logic
- I/O operations - File handling, network calls, or async operations
- NPM/external libraries - Using third-party packages not available in Q-lib
- Complex imperative logic - Loops, recursion, or stateful algorithms that can't be expressed declaratively
Important: Prefer PythonActor when CLI tools are needed. If the workflow needs to run command line applications available on the Lambda image (e.g., git, aws-cli, jq, ImageMagick, tar), use PythonActor. DenoActor has no shell access. PythonActor can invoke these tools via subprocess.run().
If the task can be done with ${{ }} expressions and Q-lib functions, use MessageProcessorActor.
Important: Avoid overusing DenoActor for data transformation. Any actor's vars and outputs configuration sections support ${{ }} expressions. Use vars for intermediate computations and outputs for formatting the final result. Only reach for DenoActor when you need fetch, I/O, or imperative logic.
| Scenario |
Use |
| Single API call |
HttpRequestActor |
| Multiple sequential API calls that depend on each other |
DenoActor or PythonActor |
| Data transformation with expressions and Q-lib |
MessageProcessorActor (inject) |
| Data transformation requiring fetch, I/O, or NPM libraries |
DenoActor or PythonActor |
| API call + data processing |
DenoActor or PythonActor |
| Data science / ML operations (pandas, numpy, scikit-learn) |
PythonActor |
| Custom Python code execution |
PythonActor |
| Shell command execution (git, aws-cli, jq, ImageMagick, tar, etc.) |
PythonActor |
| Text generation, summarization, or classification |
AiActor |
| Structured data extraction from unstructured text |
AiActor |
| Multi-turn conversations or chatbot interactions |
AiActor |
| AI with function/tool calling (single call, returns tool calls) |
AiActor |
| Autonomous AI agent with tool execution loop |
AiAgentActor |
| Complex tasks requiring multiple tool calls |
AiAgentActor |
| Research agents that search and synthesize information |
AiAgentActor |
| AI-driven file/data processing (unzip, script, edit, re-zip) |
AiAgentActor (built-in filesystem + bash) |
| AI agent that writes AND runs code |
AiAgentActor |
| Resumable AI sessions across invocations |
AiAgentActor (sessionId) |
| Agent needing MCP servers, daemons, or a full sandbox VM |
AgentHarnessActor |
| Multi-agent systems with sub-agents |
AiAgentActor (with CallFlowActor tools) |
| Route messages based on AI classification |
AiRouterActor |
| Intent detection with branching workflows |
AiRouterActor |
| If/else branching with boolean conditions |
RouterActor |
| Switch-case routing based on data values |
RouterActor |
| Inject constants or computed values |
MessageProcessorActor (inject) |
| Delay workflow execution |
MessageProcessorActor (delayBySeconds, delayUntil) |
| Process array items individually |
MessageProcessorActor (split) |
| Recombine processed array items |
MessageProcessorActor (collect) |
| Deduplicate messages |
MessageProcessorActor (dedupeByCount, dedupeByTime) |
| Filter messages conditionally |
MessageProcessorActor (filter) |
| Run parallel paths and join results |
MessageProcessorActor (fork, forkJoin) |
| Human-in-the-loop approval workflows |
MessageProcessorActor (issueCallbackToken, waitForCallbackToken) |
| Render LiquidJS templates |
MessageProcessorActor (renderTemplate) |
| Extract data with regex |
MessageProcessorActor (regexExtract) |
| Get file download URL or base64 content |
MessageProcessorActor (downloadFileUrl, downloadFileAsBase64) |
| Return dynamic HTTP response to webhook caller |
WebhookResponseActor |
| Return data from sub-flow to parent flow (requires CallableTriggerActor) |
CallableResponseActor |
| Invoke a sub-flow and wait for response |
CallFlowActor |
| Fire-and-forget sub-flow execution |
CallFlowActor (waitForResponse: false) |
| Call sub-flows in other workspaces or canvases |
CallFlowActor |
| Display a form mid-workflow and capture user input |
InterfaceActor |
| Send a form URL via email/Slack for async user input |
InterfaceActor |
| Build approval workflows without InterfaceTriggerActor |
InterfaceActor |
| Send notification emails |
SendEmailActor |
| Distribute reports via email with attachments |
SendEmailActor |
| Send HTML formatted emails |
SendEmailActor |
| Store structured data persistently |
CollectionActor (putItem, getItem) |
| Model an app's entities (users, orders, comments, …) |
CollectionActor — one collection per app, entity key prefixes (single-collection design) |
| Query stored data |
CollectionActor (query) |
| Batch read/write operations |
CollectionActor (batchGetItem, batchWriteItem) |
| Atomic counter increment/decrement |
CollectionActor (updateItem with atomicCounters) |
| Transactional operations |
CollectionActor (transactWrite, transactGet) |
| Job queue / task queue |
CollectionActor (queue pattern — putItem to enqueue, query + updateItem to dequeue) |
| Record events in order (webhook events, audit trail, activity feed, agent progress) |
StreamActor (appendData) — not event:<timestamp> Collection items |
| Process a backlog incrementally / resume where the last run stopped |
StreamActor (readStream from a cursor persisted in a Collection; loop nextCursor while hasMore) |
| Only run when new records arrived |
StreamActor (getStreamInfo — compare tailCursor to the persisted cursor) |
| Look up or update the current value of something |
CollectionActor — a stream is not a place for current state |
Trigger Actor Types
Trigger actors start workflows (flowruns). Each workflow must have exactly one trigger, but a canvas can contain multiple workflows, each with its own trigger.
| Type |
Description |
Reference |
| ButtonTriggerActor |
Manual trigger via button click in the UI |
button-trigger-actor.md |
| WebhookTriggerActor |
Receives HTTP requests at a unique webhook URL |
webhook-trigger-actor.md |
| EmailTriggerActor |
Receives emails at a unique email address |
email-trigger-actor.md |
| InterfaceTriggerActor |
Displays a web form and triggers on submission |
interface-trigger-actor.md |
| AppTriggerActor |
Hosts a web application (HTML/CSS/JS) with no form semantics. Does not emit messages. |
app-trigger-actor.md |
| ScheduledTriggerActor |
Runs on a cron-based schedule |
scheduled-trigger-actor.md |
| UniversalTriggerActor |
Code-first trigger that fires on webhook requests, a cron schedule, or manual Invoke — user TypeScript (receive(req: TriggerRequest)) runs on every fire and branches on req.trigger.type |
universal-trigger-actor.md |
| CallableTriggerActor |
Invoked by parent flows (sub-flow entry point) |
callable-trigger-actor.md |
Choosing a Trigger Type
| Scenario |
Use |
| Manual/ad-hoc execution |
ButtonTriggerActor |
| External service notifications (GitHub, Stripe, Slack) |
WebhookTriggerActor |
| Build an API endpoint |
WebhookTriggerActor |
| Process incoming emails |
EmailTriggerActor |
| User-facing forms and data collection |
InterfaceTriggerActor |
| Web applications (SPA, dashboards, interactive tools) |
AppTriggerActor |
| Periodic/scheduled tasks (hourly, daily, weekly) |
ScheduledTriggerActor |
| One workflow fired by webhook and schedule (and manual testing) |
UniversalTriggerActor |
| Custom code at trigger time (normalize, filter, dedupe, respond before emitting) |
UniversalTriggerActor |
| Reusable sub-flows called by other workflows |
CallableTriggerActor |
Universal Trigger vs Webhook Trigger (HTTP endpoints)
Both build HTTP endpoints, but they sit at opposite ends of a spectrum: a UniversalTriggerActor is the whole handler (request parsing, auth, storage, validation, and response all run inside its receive code), while a WebhookTriggerActor is the entrance to a multi-actor flow that does the work downstream.
| Decision |
Use |
| The endpoint can fully handle request parsing, auth checks, Collection API calls, validation, and the response from its own code |
UniversalTriggerActor (respond with Signal.webhookRespond under options.webhook.respondImmediately: false) |
| The request needs to enter a multi-actor flow — especially AiActor, integration actors (HttpRequestActor/template actors), routers, or a WebhookResponseActor |
WebhookTriggerActor |
| One canvas exposes several endpoints with materially different latency, response, or orchestration needs |
Multiple triggers — one per endpoint, mixing Universal and Webhook as each route requires |
Rules of thumb:
- Self-contained CRUD / lookups → Universal. If a route is "parse the request, read/write a Collection, return JSON," keep it inside one UniversalTriggerActor and respond from its code. No edges, no downstream actors.
- Orchestration → Webhook. The moment a route needs an LLM call, a third-party API, conditional routing, or a fan-out/fork, use a WebhookTriggerActor feeding the real actors and a WebhookResponseActor (or AiActor → WebhookResponseActor) for the reply.
- Don't collapse endpoints into one Universal Trigger just to reduce actor count. If even one route needs downstream actor orchestration, give that route its own WebhookTriggerActor rather than forcing AI/integration logic into trigger code. Mixed canvases (some Universal routes, some Webhook routes) are normal and correct.
URL wiring note: the two trigger types expose their URLs under different context maps — ${{ ctx.canvas.webhookTriggers.<msgVar>.url }} for a WebhookTriggerActor, ${{ ctx.canvas.universalTriggers.<msgVar>.url }} for a UniversalTriggerActor (which appears there only when configuration.webhook.enabled: true). The URL shape is identical; only the map differs.
See universal-trigger-actor.md and webhook-trigger-actor.md for full configuration. For app frontends calling these endpoints, the borgiq-react-app-builder spoke owns the wiring (see Web apps and forms — handed off to spokes).
Trigger Output
All triggers emit a message accessible to downstream actors via msg.<trigger_msgVar>. The message structure varies by trigger type—see the TypeScript schemas in references/typescript/actor-schemas-triggers.md for exact definitions:
- ButtonTriggerActor: Emits the configured
options payload
- WebhookTriggerActor: Emits
{ meta, method, headers, body, queryParams, rawBody?, response? }
- EmailTriggerActor: Emits
{ messageId, from, to, subject, date, hasAttachments, textBody, htmlBody, attachments, headers }
- InterfaceTriggerActor: Emits
{ meta: { submissionInterfaceId, user }, body: { ...field values... } }
- AppTriggerActor: Does not emit messages (no downstream workflow). Hosts a web application only.
- ScheduledTriggerActor: Emits
{ triggeredAt, lastTriggeredAt }
- UniversalTriggerActor: Emits whatever
results the user code returns (free-form; results: undefined emits nothing)
- CallableTriggerActor: Emits the payload passed by the parent flow
Important: When a task requires multiple HTTP requests stitched together, use a DenoActor or PythonActor instead of chaining multiple HttpRequestActors. See multi-api-examples.md for TypeScript and Python examples.
Common Actor Structure
All actors share a common YAML structure.
Important: Do not confuse actor-level schemas with actor-specific schema options.
actors.ACTRxxxxx.schemas.inputs and actors.ACTRxxxxx.schemas.outputs define the actor's reusable interface—what inputs the actor accepts and what outputs it produces. These are used for templatization and validation at the actor boundary.
actors.ACTRxxxxx.configuration.options.inputSchema or outputSchema are actor-specific configuration options with different purposes. For example, AiActor's configuration.options.outputSchema tells the AI model to produce structured output matching that schema—it's a directive to the LLM, not a definition of the actor's interface.
metadata:
schemaVersion: v1.0
source: BIQCanvas
actors:
ACTR01xxxxx:
type: HttpRequestActor # or DenoActor, AiActor
version: 1
name: Actor Name Here
msgVar: actor_name_here
description: What this actor does
isActive: true
continueOnError: false
enableLTM: false
enableSTM: false
sourcePorts:
- id: SPRTdefault
configuration:
inputs:
# Map upstream data and parameters here, e.g.
userId: ${{ msg.fetch_user.id }}
limit: 50
# vars: (optional — only if you need to reuse a derived value within this actor)
# - intermediateName: ${{ Q.lo.camelCase(inputs.userId) }}
options:
# Actor-type-specific options — reference ${{ inputs.* }} (or ${{ vars.* }} if defined)
outputs: ${{ results.body }}
connection:
key: connection-key-from-workspace
error:
if: ${{ error_condition }}
retryIf: ${{ retry_condition }}
message: ${{ error_message }}
schemas:
inputs:
type: object
properties:
fieldName:
type: string
title: Field Title
description: Field description
required:
- fieldName
id: ACTR01xxxxx
position:
x: 0
'y': 0
edges: {}
Actor Naming Conventions
Use concise, descriptive names with proper noun capitalization.
Good names:
- Fetch user profile from Gmail
- Create Issue in GitHub
- Process calendar events
- Transform data for Airtable
Bad names:
- Gmail: Fetch user profile (wrong format)
- Create Issue (missing context)
- Find users (too vague)
Configuration Interpolation Order
BorgIQ actors are designed to be templatized and reusable. Configuration sections are processed in order:
inputs — The actor's parameter surface. Map all upstream actor data here using ${{ msg.<upstream_msgVar>.field }}, ${{ ctx.* }}, or ${{ err.* }}. Every parameter the actor consumes should pass through inputs and be declared in schemas.inputs. Inputs are interpolated first.
vars — Optional. Intermediate values reused within this actor's own options/outputs. Only add vars when the same derived value is referenced from more than one place inside the actor (e.g. building an email body that's then base64-encoded and referenced from options). Can reference inputs. If the value is used once, inline it instead — vars is not a wiring layer.
options — Actor-specific configuration. Has access to inputs, vars, msg, ctx, and err.
Actor executes — Results are stored in results.
error — Error handling. Has access to results. Determines if the actor failed and whether to retry.
outputs — Output transformation. Only evaluated if no error. Transforms results for downstream actors.
inputs vs vars — the rule
Inputs are the wire. vars is local scratch space. Mapping upstream msg.* data into vars is wrong even though both can hold any expression: it leaves the actor's declared input schema empty, breaks reusability, and forces options/prompt/body to reference vars.X instead of the actor's real parameter surface.
Anti-pattern (do not generate this):
configuration:
vars:
- name: ${{ msg.normalize_lead.name }} # WRONG — upstream data belongs in inputs
- company: ${{ msg.normalize_lead.company }}
inputs:
name: '' # WRONG — declared inputs left empty
company: ''
options:
prompt: 'Research ${{ vars.name }} at ${{ vars.company }}' # WRONG — should reference inputs
Correct:
configuration:
inputs:
name: ${{ msg.normalize_lead.name }}
company: ${{ msg.normalize_lead.company }}
options:
prompt: 'Research ${{ inputs.name }} at ${{ inputs.company }}'
# No vars needed — single-use values stay inline.
Correct use of vars (intermediate reused inside the actor):
configuration:
inputs:
from: ${{ msg.trigger.from }}
to: ${{ msg.trigger.to }}
body: ${{ msg.trigger.body }}
vars:
- rawEmail:
- 'From: ${{ inputs.from }}'
- 'To: ${{ inputs.to }}'
- ''
- ${{ inputs.body }}
- encoded: ${{ Q.toBase64(vars.rawEmail.join('\r\n')) }}
options:
body:
raw: ${{ vars.encoded }} # vars.encoded is reused; building it inline would duplicate logic
BorgIQ Expressions
Use ${{ <javascript-expression> }} for Deno-compatible JavaScript expressions. Only YAML values can contain expressions.
Available: Q.* utility functions (see q-lib.md), all JavaScript web standard globals (btoa, JSON.parse, Math.*, array/string methods, etc.)
Restrictions: NO I/O operations (fetch, file system). Pure computation only.
Examples:
url: https://api.example.com/users/${{ inputs.userId }}
body: ${{ Q.toJSON(inputs.data) }}
data: ${{ msg.previous_actor.body }}
Context Variables
See references/context.md for full documentation.
| Variable |
Description |
inputs |
Actor input parameters |
msg |
Upstream actor messages (msg.ActorName) |
ctx |
Runtime context (org, workspace, canvas, flowrun, actor info) |
credentials |
Mapped credentials from workspace |
connection |
Single connection for authentication |
connections |
Multiple connections (access via connections.auth) |
results |
Response after actor invocation |
vars |
Computed variables |
err |
Error information from upstream actors |
For error handling patterns (continueOnError, split/collect, fork/forkJoin), see error-handling.md.
Q-lib Functions
Access utility functions via Q.*. See references/q-lib.md for complete reference.
Common functions: Q.toJSON(), Q.toBase64(), Q.isHTTPStatusInRange(), Q.lo.* (Lodash), Q.dateFns.* (date-fns)
Actor Source Files
Code-running actors — DenoActor, DenoTestActor, UniversalTriggerActor, PythonActor — carry their source in configuration.codeDir: a list of {path, content} files forming a small project, a sibling of options and never interpolated.
configuration:
options: {}
codeDir:
- path: main.ts # required entrypoint (main.py for PythonActor)
content: |
import type { Request, Response } from "@borgiq/actors";
import { format } from "./lib/format.ts";
export default async function receive(req: Request): Promise<Response> {
return { results: format(req.inputs) };
}
- path: lib/format.ts
content: |
export const format = (inputs: unknown) => ({ inputs });
- Exactly one entry must be the entrypoint:
main.ts for the three Deno-family types, main.py for PythonActor. Everything else is yours to arrange in folders.
- Import your own files relatively —
./lib/format.ts in Deno (extension included), from lib.format import format in Python (packages need __init__.py). Imports may not leave the actor's own files.
${{ }} inside source is literal text, never an expression: pass runtime values through configuration.inputs and read req.inputs.
- Some filenames are reserved by the runtime, and the tree is capped at 200 files / 1 MiB. Per-type details: deno-actor.md → Code Files, python-actor.md → Code Files, universal-trigger-actor.md → Code Files. In a canvas bundle the same tree is real files under the actor's
code/ directory (canvas-bundles.md).
- Imports may not leave the actor's own files, and this is enforced. A relative import that
escapes the actor's tree fails when the actor loads — the error tells the user that the actor
imports something outside its own code directory, and (on a deployed workspace) the build names the
specifier. Use relative imports between your own files,
@borgiq/actors, npm:/jsr:/node:
packages, or an approved https: host.
- Pin
npm: and jsr: versions exactly — npm:escape-html@1.0.3, never bare or a ^ range. On
a deployed workspace a build resolves each specifier once and every run uses that resolution, so an
unpinned specifier makes what you get depend on when the canvas was last built.
- Actors written before multi-file support carry a single
configuration.code string instead. They keep running and convert on the next save; write codeDir for anything new, and never set both fields.
(ReactAppTriggerActor also uses configuration.codeDir, for a whole Vite project — see the borgiq-react-app-builder spoke. AppTriggerActor keeps configuration.options.html / .css / .script.)
On a deployed workspace, a canvas's code actors are compiled ahead of time and every run —
triggers and editor test runs alike — executes that build rather than the canvas's current code, so
an edit takes effect only after the next build; a canvas with no fully successful build cannot run
at all. See references/deployment.md.
Actor Memory
Code-running actors (DenoActor, PythonActor, UniversalTriggerActor) carry two
key-value memory stores, STM and LTM. Every actor has both, alwa
…(truncated)
1---2name: borgiq-builder3description: Build Actors, Triggers, AI Agents, and web apps for BorgIQ. Supports HttpRequestActor, DenoActor, PythonActor, AiActor, AiAgentActor (serverless coding agent with filesystem/bash, sessions, and BorgIQ tools), AgentHarnessActor (sandboxed Claude Code with session persistence), CollectionActor, StreamActor, AppTriggerActor, InterfaceTriggerActor, WebhookTriggerActor. Use for workflow automations, REST API integrations, custom Deno/Python actors, AI-powered tasks, autonomous AI agents with tools, agent harness sandboxed execution, triggers (scheduled, webhook, email, button, interface, app, callable), or web apps with actor-backed APIs. Triggers on "create an actor", "build HTTP request", "write Deno/Python code", "use AI to process", "build an AI agent", "agent harness", "run Claude Code in sandbox", "store data", "collection", "stream", "append-only log", "set up webhook", "build a web app", "theme an app", or workflow tasks.4---56# BorgIQ Builder78Build Actors and Triggers that power BorgIQ automation workflows.910## Table of Contents1112- [BorgIQ Platform Overview](#borgiq-platform-overview)13- [Routing to Specialized Skills](#routing-to-specialized-skills)14- [TypeScript Definitions](#typescript-definitions)15- [Task Actor Types](#task-actor-types)16- [Trigger Actor Types](#trigger-actor-types)17- [Common Actor Structure](#common-actor-structure)18- [Actor Naming Conventions](#actor-naming-conventions)19- [Configuration Interpolation Order](#configuration-interpolation-order)20- [BorgIQ Expressions](#borgiq-expressions)21- [Context Variables](#context-variables)22- [Q-lib Functions](#q-lib-functions)23- [Actor Source Files](#actor-source-files)24- [Actor Memory](#actor-memory)25- [Authentication](#authentication)26- [Actor ID, Validation, and Post-Processing](#actor-id-validation-and-post-processing)27- [Generation Instructions](#generation-instructions)28- [Workflow Composition](#workflow-composition)29- [Actor Connections and Edges](#actor-connections-and-edges)30- [Workflow Examples](#workflow-examples)31- [Workflow Patterns](#workflow-patterns)32 - [Web apps and forms — handed off to spokes](#web-apps-and-forms--handed-off-to-spokes)33 - [Collection migrations and provisioning](#collection-migrations-and-provisioning)34 - [Streams: events in order, consumed by cursor](#streams-events-in-order-consumed-by-cursor)35- [Editing Existing Workflows](#editing-existing-workflows)36- [Migration from Other Platforms](#migration-from-other-platforms)37- [Deploying and Testing with the CLI](#deploying-and-testing-with-the-cli)38 - [Canvas Bundles](references/cli/canvas-bundles.md)39 - [CLI Command Reference](references/cli/cli-command-reference.md)40 - [CLI Data Formats](references/cli/cli-data-formats.md)41 - [CLI Scaffolding Scripts](references/cli/cli-setup-scripts.md)42 - [CLI Troubleshooting](references/cli/cli-troubleshooting.md)4344## BorgIQ Platform Overview4546BorgIQ is an automation platform where nodes are called **Actors**. Workflows chain Actors together with connections (edges). Each actor emits messages stored under `msg.ActorName`.4748### Workspaces and Canvases4950BorgIQ organizes workflows in a hierarchical structure:5152| Concept | Description | Identifier |53|---------|-------------|------------|54| **Workspace** | A container for canvases, connections, and team members. Workspaces provide isolation and access control. | `workspaceSlug` (e.g., `my-team`, `prod-ws`) |55| **Canvas** | A container for one or more workflows and their connections. Each workflow within a canvas has its own trigger actor. | `canvasSlug` (e.g., `process-orders`, `send-notifications`) |56| **Actor** | An individual node within a canvas that performs work or triggers execution. | `actorId` (e.g., `ACTR01kd6tesvky0mh8x1css3sv5yg`) |5758**Slug Format:**59- Workspace slugs: 5-10 lowercase alphanumeric characters with hyphens (e.g., `john-dev`)60- Canvas slugs: 2-255 lowercase alphanumeric characters with hyphens (e.g., `skill-test`)6162**Cross-Canvas/Workspace Calls:**63Actors can invoke sub-flows in other canvases or workspaces using CallFlowActor. When `workspaceSlug` or `canvasSlug` is omitted, the current workspace/canvas is assumed.6465### Actor Categories6667There are two categories of actors:6869| Category | Description | Examples |70|----------|-------------|----------|71| **Trigger Actors** | Start workflows (flowruns). Each workflow has exactly one trigger. A canvas can contain multiple workflows, each with its own trigger. | ButtonTriggerActor, WebhookTriggerActor, ScheduledTriggerActor |72| **Task Actors** | Perform work within the workflow. Process data, make API calls, route messages, etc. | HttpRequestActor, DenoActor, AiActor, RouterActor |7374**Example workflow:** `TriggerActor -> TaskActor1 -> TaskActor2` produces:75```json76{77 "msg": {78 "trigger_actor": { "...output from trigger..." },79 "task_actor_1": { "...output from task 1..." },80 "task_actor_2": { "...output from task 2..." }81 }82}83```8485### Concurrent Execution Model8687**BorgIQ executes all downstream actors concurrently by default.** When an actor emits a message, all connected downstream actors start executing in parallel without any explicit configuration.8889```90 A91 / \92 B C <- B and C run concurrently when A emits93 \ /94 D <- D receives TWO separate messages (one from B, one from C)95```9697**Key behavior:**98- If A connects to both B and C, both B and C execute concurrently when A emits99- D will receive **two separate messages** and execute **twice** (once for B's output, once for C's output)100- No `fork` action is needed for parallel execution—it happens automatically101102**When to use `fork`/`forkJoin`:**103104Only use the `fork` and `forkJoin` MessageProcessorActor actions when you need to **synchronize** parallel paths and emit a **single combined message**. See [message-processor-actor.md](references/message-processor-actor.md#fork-actions) for detailed documentation, [workflow-patterns.md](references/workflow-patterns.md#pattern-1-multi-source-data-aggregation) for complete examples, and [fork-join-common-mistakes.md](references/fork-join-common-mistakes.md) for common pitfalls to avoid.105106| Scenario | Use Fork/ForkJoin? |107|----------|-------------------|108| Run B and C in parallel, D processes each separately | **No** - just connect A→B→D and A→C→D |109| Run B and C in parallel, D needs combined results from both | **Yes** - use `fork` before split, `forkJoin` to recombine |110| Fire-and-forget parallel notifications (email + Slack) | **No** - just connect to both actors |111| Parallel API calls where you need all results together | **Yes** - use `fork`/`forkJoin` pattern |112113**Critical rules:**114- `forkJoin` requires `enableSTM: true`115- MessageProcessorActor always uses only `SPRTdefault` (fork uses multiple edges, not multiple sourcePorts)116- Only RouterActor, AiRouterActor, InterfaceActor, AiAgentActor, and AgentHarnessActor use multiple sourcePorts117118## Routing to Specialized Skills119120This skill is the **hub** of the `borgiq-builder` plugin. It covers actor wiring, edges, msgVars, expressions, and overall workflow composition. Four **spoke** skills ship in the same plugin and load automatically when their domain appears in the user's request. Pull them in actively when the work crosses into their area — they're more opinionated and focused than this hub.121122| Spoke | Load when the user is doing… | What it owns |123|---|---|---|124| **`borgiq-form-builder`** | Interface pages, forms, signups, surveys, approval forms, data-entry UIs, web-viewer embeds | InterfaceTriggerActor + InterfaceActor + form components + themes + webViewer styling |125| **`borgiq-react-app-builder`** | Custom app UIs — dashboards, data explorers, SPAs, any component-based or multi-file frontend, npm UI libraries, `useEndpoint`, `useGetSession` (who is viewing the app), `useStreamTail` (follow a workspace stream live); also owns maintaining legacy raw-HTML AppTriggerActor apps | ReactAppTriggerActor + server-side Vite build + `codeDir`/`options.files` model + `@borgiq/actors` SDK + webhook endpoints + declared stream tails + viewer session + the app theme library |126| **`borgiq-agent-builder`** | Autonomous AI behavior — AiAgentActor (serverless coding agent with filesystem/bash + tools), AgentHarnessActor (sandboxed Claude Code), McpServerActor (expose tools to external agents) | AiActor when-not-to-use + AiAgentActor + AgentHarnessActor + McpServerActor |127| **`borgiq-json-schema-builder`** | Non-trivial JSON schemas — AiActor `outputSchema`, agent tool input schemas, Collection item schemas, Callable response schemas | All schema-design decisions, anti-patterns, and the BorgIQ `type: any` convention |128129Cross-domain example: _"build a flow with an interface form that takes a customer name, hands it to an AI agent that researches them, posts the result to Slack"_ → hub orchestrates + `borgiq-form-builder` designs the form + `borgiq-agent-builder` designs the agent + `borgiq-json-schema-builder` defines the research output schema. The hub stays in context throughout and handles wiring, edges, IDs, and the Slack HTTP actor.130131## TypeScript Definitions132133Complete TypeScript/Zod schema definitions for all actors are available in [references/typescript/](references/typescript/). Use these to understand exact data structures, validation rules, and type constraints.134135| Reference File | Description |136|-------------|-------------|137| [actor-schemas-triggers.md](references/typescript/actor-schemas-triggers.md) | Trigger actor options and results (Button, Webhook, Email, Interface, App, Scheduled, Universal, Callable) |138| [actor-schemas-task-core.md](references/typescript/actor-schemas-task-core.md) | Core task actor schemas (AiActor, AiAgentActor, DenoActor, PythonActor, RouterActor, etc.) |139| [actor-schemas-task-http.md](references/typescript/actor-schemas-task-http.md) | HttpRequestActor options and authentication types |140| [actor-schemas-task-datastore.md](references/typescript/actor-schemas-task-datastore.md) | DataStoreActor actions (legacy — kept for TypeScript type reference) |141| [actor-schemas-task-collection.md](references/typescript/actor-schemas-task-collection.md) | CollectionActor actions (query, getItem, putItem, batchGet, batchWrite, etc.) |142| [actor-schemas-task-stream.md](references/typescript/actor-schemas-task-stream.md) | StreamActor actions (createStream, appendData, readStream, getStreamInfo, etc.) |143| [actor-schemas-task-messageprocessor.md](references/typescript/actor-schemas-task-messageprocessor.md) | MessageProcessorActor actions (inject, split, collect, fork, forkJoin, delay, etc.) |144| [actor-schemas-comment.md](references/typescript/actor-schemas-comment.md) | CommentActor schema |145| [form-components.md](references/typescript/form-components.md) | Interface form component schemas (InterfaceTriggerActor and InterfaceActor only, not used by AppTriggerActor) |146| [schemas.md](references/typescript/schemas.md) | Common schemas (IDs, files, runtime types, context, signals) |147| [common-types.md](references/typescript/common-types.md) | Shared types, AI model definitions, canvas, runtime, sandbox types |148149**Usage:** When building actors or understanding output structures, read the relevant TypeScript reference markdown file to find exact field names, types, and validation rules. Each file contains a table of contents linking to individual type definitions.150151## Task Actor Types152153| Type | Description | Reference |154|------|-------------|-----------|155| **HttpRequestActor** | Makes REST API calls to external services (Gmail, GitHub, Airtable, etc.) | [http-request-actor.md](references/http-request-actor.md) |156| **DenoActor** | Executes custom TypeScript/JavaScript code in a sandboxed Deno runtime | [deno-actor.md](references/deno-actor.md) |157| **PythonActor** | Executes custom Python code in a sandboxed Python runtime with UV package management | [python-actor.md](references/python-actor.md) |158| **AiActor** | Invokes AI models (LLMs) for text generation, structured output, and AI-powered tasks | [ai-actor.md](references/ai-actor.md) |159| **AiAgentActor** | Autonomous AI coding agent running in checkpointed serverless segments. Has a private workspace with built-in filesystem/bash tools (`read`/`write`/`edit`/`bash`/`grep`/`find`/`ls`, plus an opt-in `deno` tool for running code) plus BorgIQ actors as tools, session continuation via `sessionId`, and workspace zip in/out (`volumeZipFile` → `outputZipFile`). Has two output ports: Done (final result + zips) and Status (assistant turns + tool results). Tool actors are rendered inside the agent boundary with empty edges. | [ai-agent-actor.md](references/ai-agent-actor.md) |160| **DeprecatedAiAgent** | Legacy orchestrator-loop AI agent (pre-2026 `AiAgentActor`) — no filesystem or sessions. Hidden from the palette; existing instances keep running. **Do not create new instances — use AiAgentActor.** | [deprecated-ai-agent.md](references/deprecated-ai-agent.md) |161| **AgentHarnessActor** | Runs Claude Code in an isolated sandbox (E2B or Daytona) with full filesystem access, code execution, session persistence via `sessionId`, and queued inbound messages. Supports `volumeZipFile` for context, network control, MCP servers, environment variables, and returns workspace + session data zips. Use when the agent needs to execute code, install packages, or persist state across sessions. Has two output ports: Done (final result with output files) and Status (real-time execution updates). | [agent-harness-actor.md](references/agent-harness-actor.md) |162| **AiRouterActor** | Routes messages to different outputs based on AI-powered classification | [ai-router-actor.md](references/ai-router-actor.md) |163| **RouterActor** | Routes messages based on boolean conditions (if/else, switch logic) | [router-actor.md](references/router-actor.md) |164| **MessageProcessorActor** | Processes, transforms, and controls message flow (inject data, delay, split/collect arrays, dedupe, filter, fork/forkJoin, callbacks). **Important:** Always has only `SPRTdefault` sourcePort—fork uses multiple edges to create parallel paths, not multiple sourcePorts. | [message-processor-actor.md](references/message-processor-actor.md) |165| **WebhookResponseActor** | Sends custom HTTP responses back to WebhookTriggerActor callers | [webhook-response-actor.md](references/webhook-response-actor.md) |166| **CallableResponseActor** | Returns data from sub-flows back to parent flows (CallFlowActor). **Only valid in flows triggered by CallableTriggerActor.** | [callable-response-actor.md](references/callable-response-actor.md) |167| **CallFlowActor** | Invokes sub-flows by calling a CallableTriggerActor in another canvas/workspace | [call-flow-actor.md](references/call-flow-actor.md) |168| **InterfaceActor** | Renders a web form/page mid-workflow with two output ports: Meta (URL info on render) and Event (form submission data) | [interface-actor.md](references/interface-actor.md) |169| **SendEmailActor** | Sends text/HTML emails with optional attachments | [send-email-actor.md](references/send-email-actor.md) |170| **CollectionActor** | Persistent structured storage organized into named collections with labels, TTL, queries, batch operations, and transactions. Recommended for all new storage needs. **One collection per app** — model all entity types with key prefixes ([single-collection design](references/collection-api.md#single-collection-design)). | [collection-actor.md](references/collection-actor.md) |171| **StreamActor** | Append-only, ordered, cursor-addressed record logs — event ingestion, audit trails, activity feeds, and incremental processing that resumes from a persisted cursor. Reads return **one bounded page**, never the stream. **Streams must be created before use** and **expire one hour after the last append** unless created `persistent: true` or with an explicit `idleTtlSeconds`. Use for "what happened, in order"; use CollectionActor for "the current value of X" ([Collections vs Streams](references/stream-api.md#collections-vs-streams)). | [stream-actor.md](references/stream-actor.md) |172| **McpServerActor** | Exposes its child tool actors as an [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server endpoint that external AI agents (Claude Desktop, Cursor, custom agents) can connect to. Reuses the AiAgentActor tool-actor pattern (`aiAgentToolActorIds`, `${{aiInput}}` schema filtering) — the difference is that an external MCP client drives tool invocations instead of an internal LLM loop. | [mcp-server-actor.md](references/mcp-server-actor.md) |173| **CommentActor** | Non-functional UI element for adding notes, TODOs, and documentation to workflows | [comment-actor.md](references/comment-actor.md) |174175> **⚠️ Before hand-building an integration actor — especially an `HttpRequestActor` — search the template catalog first.** BorgIQ ships vetted templates for most third-party actions (Gmail, Slack, GitHub, Google, Notion, …). Adapting a template is the single biggest defense against the most common failure mode: hand-writing an actor's `options`, `sourcePorts`, and schemas from scratch and getting them subtly wrong. **Only hand-build when no template fits.**176>177> ```bash178> borgiq templates apps --search "<vendor>" --json # run a few queries, e.g. gmail, google, slack179> borgiq templates list --app-id TAPP... --json # list that app's templates (paginates/sorts)180> borgiq templates get ATMP... --json \ # fetch the chosen template, then convert it:181> | borgiq scaffold actor-from-template --output actor.json --print-id182> borgiq canvas-actors create <canvasSlugOrId> <actorId> --file actor.json --json183> ```184>185> In a bundle, write the `templates get` actor payload (already ExportedCanvasActor object shape) as `actor.yaml`, apply the [template fixups](references/cli/canvas-bundles.md#templates-and-the-starter-limitation) (fresh actor ID and trigger keys, keep `template` provenance), and complete the [three-edit rule](references/cli/canvas-bundles.md#add-and-remove-actors-the-three-edit-rule). `scaffold actor-from-template` produces the YAML-string CanvasActor mutation shape for the direct/batch fallback and performs those fixups automatically. Full flow: [Deploying and Testing with the CLI](#deploying-and-testing-with-the-cli).186187### Choosing a Task Actor Type188189**Important: DenoActor vs MessageProcessorActor**190191Use **MessageProcessorActor** as the default for data transformations. It handles most transformation needs via YAML configuration and `${{ }}` expressions without custom code.192193Use **DenoActor** (or PythonActor) ONLY when you need:194- **Fetch/HTTP requests** - Making API calls within custom logic195- **I/O operations** - File handling, network calls, or async operations196- **NPM/external libraries** - Using third-party packages not available in Q-lib197- **Complex imperative logic** - Loops, recursion, or stateful algorithms that can't be expressed declaratively198199**Important: Prefer PythonActor when CLI tools are needed.** If the workflow needs to run command line applications available on the Lambda image (e.g., `git`, `aws-cli`, `jq`, `ImageMagick`, `tar`), use **PythonActor**. DenoActor has no shell access. PythonActor can invoke these tools via `subprocess.run()`.200201If the task can be done with `${{ }}` expressions and Q-lib functions, use MessageProcessorActor.202203**Important: Avoid overusing DenoActor for data transformation.** Any actor's `vars` and `outputs` configuration sections support `${{ }}` expressions. Use `vars` for intermediate computations and `outputs` for formatting the final result. Only reach for DenoActor when you need fetch, I/O, or imperative logic.204205| Scenario | Use |206|----------|-----|207| Single API call | HttpRequestActor |208| Multiple sequential API calls that depend on each other | **DenoActor** or **PythonActor** |209| Data transformation with expressions and Q-lib | **MessageProcessorActor** (`inject`) |210| Data transformation requiring fetch, I/O, or NPM libraries | DenoActor or PythonActor |211| API call + data processing | DenoActor or PythonActor |212| Data science / ML operations (pandas, numpy, scikit-learn) | **PythonActor** |213| Custom Python code execution | PythonActor |214| Shell command execution (git, aws-cli, jq, ImageMagick, tar, etc.) | **PythonActor** |215| Text generation, summarization, or classification | AiActor |216| Structured data extraction from unstructured text | AiActor |217| Multi-turn conversations or chatbot interactions | AiActor |218| AI with function/tool calling (single call, returns tool calls) | AiActor |219| Autonomous AI agent with tool execution loop | **AiAgentActor** |220| Complex tasks requiring multiple tool calls | **AiAgentActor** |221| Research agents that search and synthesize information | **AiAgentActor** |222| AI-driven file/data processing (unzip, script, edit, re-zip) | **AiAgentActor** (built-in filesystem + bash) |223| AI agent that writes AND runs code | **AiAgentActor** |224| Resumable AI sessions across invocations | **AiAgentActor** (`sessionId`) |225| Agent needing MCP servers, daemons, or a full sandbox VM | AgentHarnessActor |226| Multi-agent systems with sub-agents | AiAgentActor (with CallFlowActor tools) |227| Route messages based on AI classification | AiRouterActor |228| Intent detection with branching workflows | AiRouterActor |229| If/else branching with boolean conditions | RouterActor |230| Switch-case routing based on data values | RouterActor |231| Inject constants or computed values | MessageProcessorActor (`inject`) |232| Delay workflow execution | MessageProcessorActor (`delayBySeconds`, `delayUntil`) |233| Process array items individually | MessageProcessorActor (`split`) |234| Recombine processed array items | MessageProcessorActor (`collect`) |235| Deduplicate messages | MessageProcessorActor (`dedupeByCount`, `dedupeByTime`) |236| Filter messages conditionally | MessageProcessorActor (`filter`) |237| Run parallel paths and join results | MessageProcessorActor (`fork`, `forkJoin`) |238| Human-in-the-loop approval workflows | MessageProcessorActor (`issueCallbackToken`, `waitForCallbackToken`) |239| Render LiquidJS templates | MessageProcessorActor (`renderTemplate`) |240| Extract data with regex | MessageProcessorActor (`regexExtract`) |241| Get file download URL or base64 content | MessageProcessorActor (`downloadFileUrl`, `downloadFileAsBase64`) |242| Return dynamic HTTP response to webhook caller | WebhookResponseActor |243| Return data from sub-flow to parent flow (requires CallableTriggerActor) | CallableResponseActor |244| Invoke a sub-flow and wait for response | CallFlowActor |245| Fire-and-forget sub-flow execution | CallFlowActor (`waitForResponse: false`) |246| Call sub-flows in other workspaces or canvases | CallFlowActor |247| Display a form mid-workflow and capture user input | InterfaceActor |248| Send a form URL via email/Slack for async user input | InterfaceActor |249| Build approval workflows without InterfaceTriggerActor | InterfaceActor |250| Send notification emails | SendEmailActor |251| Distribute reports via email with attachments | SendEmailActor |252| Send HTML formatted emails | SendEmailActor |253| Store structured data persistently | CollectionActor (`putItem`, `getItem`) |254| Model an app's entities (users, orders, comments, …) | CollectionActor — **one collection per app**, entity key prefixes ([single-collection design](references/collection-api.md#single-collection-design)) |255| Query stored data | CollectionActor (`query`) |256| Batch read/write operations | CollectionActor (`batchGetItem`, `batchWriteItem`) |257| Atomic counter increment/decrement | CollectionActor (`updateItem` with `atomicCounters`) |258| Transactional operations | CollectionActor (`transactWrite`, `transactGet`) |259| Job queue / task queue | CollectionActor (queue pattern — `putItem` to enqueue, `query` + `updateItem` to dequeue) |260| Record events in order (webhook events, audit trail, activity feed, agent progress) | StreamActor (`appendData`) — **not** `event:<timestamp>` Collection items |261| Process a backlog incrementally / resume where the last run stopped | StreamActor (`readStream` from a cursor persisted in a Collection; loop `nextCursor` while `hasMore`) |262| Only run when new records arrived | StreamActor (`getStreamInfo` — compare `tailCursor` to the persisted cursor) |263| Look up or update the current value of something | CollectionActor — a stream is not a place for current state |264265## Trigger Actor Types266267Trigger actors start workflows (flowruns). Each workflow must have exactly one trigger, but a canvas can contain multiple workflows, each with its own trigger.268269| Type | Description | Reference |270|------|-------------|-----------|271| **ButtonTriggerActor** | Manual trigger via button click in the UI | [button-trigger-actor.md](references/button-trigger-actor.md) |272| **WebhookTriggerActor** | Receives HTTP requests at a unique webhook URL | [webhook-trigger-actor.md](references/webhook-trigger-actor.md) |273| **EmailTriggerActor** | Receives emails at a unique email address | [email-trigger-actor.md](references/email-trigger-actor.md) |274| **InterfaceTriggerActor** | Displays a web form and triggers on submission | [interface-trigger-actor.md](references/interface-trigger-actor.md) |275| **AppTriggerActor** | Hosts a web application (HTML/CSS/JS) with no form semantics. Does not emit messages. | [app-trigger-actor.md](references/app-trigger-actor.md) |276| **ScheduledTriggerActor** | Runs on a cron-based schedule | [scheduled-trigger-actor.md](references/scheduled-trigger-actor.md) |277| **UniversalTriggerActor** | Code-first trigger that fires on webhook requests, a cron schedule, or manual Invoke — user TypeScript (`receive(req: TriggerRequest)`) runs on every fire and branches on `req.trigger.type` | [universal-trigger-actor.md](references/universal-trigger-actor.md) |278| **CallableTriggerActor** | Invoked by parent flows (sub-flow entry point) | [callable-trigger-actor.md](references/callable-trigger-actor.md) |279280### Choosing a Trigger Type281282| Scenario | Use |283|----------|-----|284| Manual/ad-hoc execution | ButtonTriggerActor |285| External service notifications (GitHub, Stripe, Slack) | WebhookTriggerActor |286| Build an API endpoint | WebhookTriggerActor |287| Process incoming emails | EmailTriggerActor |288| User-facing forms and data collection | InterfaceTriggerActor |289| Web applications (SPA, dashboards, interactive tools) | AppTriggerActor |290| Periodic/scheduled tasks (hourly, daily, weekly) | ScheduledTriggerActor |291| One workflow fired by webhook **and** schedule (and manual testing) | UniversalTriggerActor |292| Custom code at trigger time (normalize, filter, dedupe, respond before emitting) | UniversalTriggerActor |293| Reusable sub-flows called by other workflows | CallableTriggerActor |294295#### Universal Trigger vs Webhook Trigger (HTTP endpoints)296297Both build HTTP endpoints, but they sit at opposite ends of a spectrum: a UniversalTriggerActor *is* the whole handler (request parsing, auth, storage, validation, and response all run inside its `receive` code), while a WebhookTriggerActor is the *entrance* to a multi-actor flow that does the work downstream.298299| Decision | Use |300|----------|-----|301| The endpoint can fully handle request parsing, auth checks, Collection API calls, validation, and the response from its own code | **UniversalTriggerActor** (respond with `Signal.webhookRespond` under `options.webhook.respondImmediately: false`) |302| The request needs to enter a multi-actor flow — especially AiActor, integration actors (HttpRequestActor/template actors), routers, or a WebhookResponseActor | **WebhookTriggerActor** |303| One canvas exposes several endpoints with materially different latency, response, or orchestration needs | **Multiple triggers** — one per endpoint, mixing Universal and Webhook as each route requires |304305**Rules of thumb:**306307- **Self-contained CRUD / lookups → Universal.** If a route is "parse the request, read/write a Collection, return JSON," keep it inside one UniversalTriggerActor and respond from its code. No edges, no downstream actors.308- **Orchestration → Webhook.** The moment a route needs an LLM call, a third-party API, conditional routing, or a fan-out/fork, use a WebhookTriggerActor feeding the real actors and a WebhookResponseActor (or AiActor → WebhookResponseActor) for the reply.309- **Don't collapse endpoints into one Universal Trigger just to reduce actor count.** If even one route needs downstream actor orchestration, give that route its own WebhookTriggerActor rather than forcing AI/integration logic into trigger code. Mixed canvases (some Universal routes, some Webhook routes) are normal and correct.310311**URL wiring note:** the two trigger types expose their URLs under **different context maps** — `${{ ctx.canvas.webhookTriggers.<msgVar>.url }}` for a WebhookTriggerActor, `${{ ctx.canvas.universalTriggers.<msgVar>.url }}` for a UniversalTriggerActor (which appears there only when `configuration.webhook.enabled: true`). The URL shape is identical; only the map differs.312313See [universal-trigger-actor.md](references/universal-trigger-actor.md) and [webhook-trigger-actor.md](references/webhook-trigger-actor.md) for full configuration. For app frontends calling these endpoints, the `borgiq-react-app-builder` spoke owns the wiring (see [Web apps and forms — handed off to spokes](#web-apps-and-forms--handed-off-to-spokes)).314315### Trigger Output316317All triggers emit a message accessible to downstream actors via `msg.<trigger_msgVar>`. The message structure varies by trigger type—see the TypeScript schemas in [references/typescript/actor-schemas-triggers.md](references/typescript/actor-schemas-triggers.md) for exact definitions:318319- **ButtonTriggerActor**: Emits the configured `options` payload320- **WebhookTriggerActor**: Emits `{ meta, method, headers, body, queryParams, rawBody?, response? }`321- **EmailTriggerActor**: Emits `{ messageId, from, to, subject, date, hasAttachments, textBody, htmlBody, attachments, headers }`322- **InterfaceTriggerActor**: Emits `{ meta: { submissionInterfaceId, user }, body: { ...field values... } }`323- **AppTriggerActor**: Does **not** emit messages (no downstream workflow). Hosts a web application only.324- **ScheduledTriggerActor**: Emits `{ triggeredAt, lastTriggeredAt }`325- **UniversalTriggerActor**: Emits whatever `results` the user code returns (free-form; `results: undefined` emits nothing)326- **CallableTriggerActor**: Emits the payload passed by the parent flow327328**Important:** When a task requires multiple HTTP requests stitched together, use a **DenoActor** or **PythonActor** instead of chaining multiple HttpRequestActors. See [multi-api-examples.md](references/multi-api-examples.md) for TypeScript and Python examples.329330## Common Actor Structure331332All actors share a common YAML structure.333334**Important: Do not confuse actor-level `schemas` with actor-specific schema options.**335336- **`actors.ACTRxxxxx.schemas.inputs`** and **`actors.ACTRxxxxx.schemas.outputs`** define the actor's reusable interface—what inputs the actor accepts and what outputs it produces. These are used for templatization and validation at the actor boundary.337338- **`actors.ACTRxxxxx.configuration.options.inputSchema`** or **`outputSchema`** are actor-specific configuration options with different purposes. For example, AiActor's `configuration.options.outputSchema` tells the AI model to produce structured output matching that schema—it's a directive to the LLM, not a definition of the actor's interface.339340```yaml341metadata:342 schemaVersion: v1.0343 source: BIQCanvas344actors:345 ACTR01xxxxx:346 type: HttpRequestActor # or DenoActor, AiActor347 version: 1348 name: Actor Name Here349 msgVar: actor_name_here350 description: What this actor does351 isActive: true352 continueOnError: false353 enableLTM: false354 enableSTM: false355 sourcePorts:356 - id: SPRTdefault357 configuration:358 inputs:359 # Map upstream data and parameters here, e.g.360 userId: ${{ msg.fetch_user.id }}361 limit: 50362 # vars: (optional — only if you need to reuse a derived value within this actor)363 # - intermediateName: ${{ Q.lo.camelCase(inputs.userId) }}364 options:365 # Actor-type-specific options — reference ${{ inputs.* }} (or ${{ vars.* }} if defined)366 outputs: ${{ results.body }}367 connection:368 key: connection-key-from-workspace369 error:370 if: ${{ error_condition }}371 retryIf: ${{ retry_condition }}372 message: ${{ error_message }}373 schemas:374 inputs:375 type: object376 properties:377 fieldName:378 type: string379 title: Field Title380 description: Field description381 required:382 - fieldName383 id: ACTR01xxxxx384 position:385 x: 0386 'y': 0387 edges: {}388```389390## Actor Naming Conventions391392Use concise, descriptive names with proper noun capitalization.393394**Good names:**395- Fetch user profile from Gmail396- Create Issue in GitHub397- Process calendar events398- Transform data for Airtable399400**Bad names:**401- Gmail: Fetch user profile (wrong format)402- Create Issue (missing context)403- Find users (too vague)404405## Configuration Interpolation Order406407BorgIQ actors are designed to be **templatized and reusable**. Configuration sections are processed in order:4084091. **inputs** — The actor's parameter surface. **Map all upstream actor data here** using `${{ msg.<upstream_msgVar>.field }}`, `${{ ctx.* }}`, or `${{ err.* }}`. Every parameter the actor consumes should pass through `inputs` and be declared in `schemas.inputs`. Inputs are interpolated first.4104112. **vars** — *Optional.* Intermediate values reused within **this actor's own** `options`/`outputs`. Only add `vars` when the same derived value is referenced from more than one place inside the actor (e.g. building an email body that's then base64-encoded and referenced from `options`). Can reference `inputs`. If the value is used once, inline it instead — `vars` is not a wiring layer.4124133. **options** — Actor-specific configuration. Has access to `inputs`, `vars`, `msg`, `ctx`, and `err`.4144154. **Actor executes** — Results are stored in `results`.4164175. **error** — Error handling. Has access to `results`. Determines if the actor failed and whether to retry.4184196. **outputs** — Output transformation. Only evaluated if no error. Transforms `results` for downstream actors.420421### inputs vs vars — the rule422423**Inputs are the wire.** `vars` is local scratch space. Mapping upstream `msg.*` data into `vars` is wrong even though both can hold any expression: it leaves the actor's declared input schema empty, breaks reusability, and forces `options`/`prompt`/`body` to reference `vars.X` instead of the actor's real parameter surface.424425**Anti-pattern (do not generate this):**426```yaml427configuration:428 vars:429 - name: ${{ msg.normalize_lead.name }} # WRONG — upstream data belongs in inputs430 - company: ${{ msg.normalize_lead.company }}431 inputs:432 name: '' # WRONG — declared inputs left empty433 company: ''434 options:435 prompt: 'Research ${{ vars.name }} at ${{ vars.company }}' # WRONG — should reference inputs436```437438**Correct:**439```yaml440configuration:441 inputs:442 name: ${{ msg.normalize_lead.name }}443 company: ${{ msg.normalize_lead.company }}444 options:445 prompt: 'Research ${{ inputs.name }} at ${{ inputs.company }}'446# No vars needed — single-use values stay inline.447```448449**Correct use of `vars` (intermediate reused inside the actor):**450```yaml451configuration:452 inputs:453 from: ${{ msg.trigger.from }}454 to: ${{ msg.trigger.to }}455 body: ${{ msg.trigger.body }}456 vars:457 - rawEmail:458 - 'From: ${{ inputs.from }}'459 - 'To: ${{ inputs.to }}'460 - ''461 - ${{ inputs.body }}462 - encoded: ${{ Q.toBase64(vars.rawEmail.join('\r\n')) }}463 options:464 body:465 raw: ${{ vars.encoded }} # vars.encoded is reused; building it inline would duplicate logic466```467468## BorgIQ Expressions469470Use `${{ <javascript-expression> }}` for Deno-compatible JavaScript expressions. Only YAML values can contain expressions.471472**Available:** `Q.*` utility functions (see [q-lib.md](references/q-lib.md)), all JavaScript web standard globals (`btoa`, `JSON.parse`, `Math.*`, array/string methods, etc.)473474**Restrictions:** NO I/O operations (`fetch`, file system). Pure computation only.475476**Examples:**477```yaml478url: https://api.example.com/users/${{ inputs.userId }}479body: ${{ Q.toJSON(inputs.data) }}480data: ${{ msg.previous_actor.body }}481```482483## Context Variables484485See [references/context.md](references/context.md) for full documentation.486487| Variable | Description |488|----------|-------------|489| `inputs` | Actor input parameters |490| `msg` | Upstream actor messages (`msg.ActorName`) |491| `ctx` | Runtime context (org, workspace, canvas, flowrun, actor info) |492| `credentials` | Mapped credentials from workspace |493| `connection` | Single connection for authentication |494| `connections` | Multiple connections (access via `connections.auth`) |495| `results` | Response after actor invocation |496| `vars` | Computed variables |497| `err` | Error information from upstream actors |498499For error handling patterns (continueOnError, split/collect, fork/forkJoin), see [error-handling.md](references/error-handling.md).500501## Q-lib Functions502503Access utility functions via `Q.*`. See [references/q-lib.md](references/q-lib.md) for complete reference.504505**Common functions:** `Q.toJSON()`, `Q.toBase64()`, `Q.isHTTPStatusInRange()`, `Q.lo.*` (Lodash), `Q.dateFns.*` (date-fns)506507## Actor Source Files508509Code-running actors — **DenoActor, DenoTestActor, UniversalTriggerActor, PythonActor** — carry their source in `configuration.codeDir`: a list of `{path, content}` files forming a small project, a sibling of `options` and **never interpolated**.510511```yaml512configuration:513 options: {}514 codeDir:515 - path: main.ts # required entrypoint (main.py for PythonActor)516 content: |517 import type { Request, Response } from "@borgiq/actors";518519 import { format } from "./lib/format.ts";520521 export default async function receive(req: Request): Promise<Response> {522 return { results: format(req.inputs) };523 }524 - path: lib/format.ts525 content: |526 export const format = (inputs: unknown) => ({ inputs });527```528529- Exactly one entry must be the **entrypoint**: `main.ts` for the three Deno-family types, `main.py` for PythonActor. Everything else is yours to arrange in folders.530- Import your own files relatively — `./lib/format.ts` in Deno (extension included), `from lib.format import format` in Python (packages need `__init__.py`). Imports may not leave the actor's own files.531- `${{ }}` inside source is literal text, never an expression: pass runtime values through `configuration.inputs` and read `req.inputs`.532- Some filenames are reserved by the runtime, and the tree is capped at 200 files / 1 MiB. Per-type details: [deno-actor.md → Code Files](references/deno-actor.md#code-files), [python-actor.md → Code Files](references/python-actor.md#code-files), [universal-trigger-actor.md → Code Files](references/universal-trigger-actor.md#code-files). In a canvas bundle the same tree is real files under the actor's `code/` directory ([canvas-bundles.md](references/cli/canvas-bundles.md#code-actor-project-trees)).533- **Imports may not leave the actor's own files, and this is enforced.** A relative import that534 escapes the actor's tree fails when the actor loads — the error tells the user that the actor535 imports something outside its own code directory, and (on a deployed workspace) the build names the536 specifier. Use relative imports between your own files, `@borgiq/actors`, `npm:`/`jsr:`/`node:`537 packages, or an approved `https:` host.538- **Pin `npm:` and `jsr:` versions exactly** — `npm:escape-html@1.0.3`, never bare or a `^` range. On539 a deployed workspace a build resolves each specifier once and every run uses that resolution, so an540 unpinned specifier makes what you get depend on when the canvas was last built.541- Actors written before multi-file support carry a single `configuration.code` string instead. They keep running and convert on the next save; write `codeDir` for anything new, and never set both fields.542543(ReactAppTriggerActor also uses `configuration.codeDir`, for a whole Vite project — see the `borgiq-react-app-builder` spoke. AppTriggerActor keeps `configuration.options.html` / `.css` / `.script`.)544545On a **deployed** workspace, a canvas's code actors are compiled ahead of time and every run —546triggers and editor test runs alike — executes that build rather than the canvas's current code, so547an edit takes effect only after the next build; a canvas with no fully successful build cannot run548at all. See [references/deployment.md](references/deployment.md).549550## Actor Memory551552Code-running actors (DenoActor, PythonActor, UniversalTriggerActor) carry two553key-value memory stores, **STM** and **LTM**. Every actor has **both**, alwa554555…(truncated)