# Borgiq Builder

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

- Skill: `borgiq/borgiq-builder` (Agent Skill, multi-file: 76 files)
- Install (CLI): `npx skillmds@latest add borgiq/borgiq-builder`
- Raw SKILL.md: https://api.skillmd.com/api/skills/borgiq/borgiq-builder/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: BorgIQ (https://skillmd.com/u/borgiq)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/borgiq/borgiq-builder

---


# BorgIQ Builder

Build Actors and Triggers that power BorgIQ automation workflows.

## Table of Contents

- [BorgIQ Platform Overview](#borgiq-platform-overview)
- [Routing to Specialized Skills](#routing-to-specialized-skills)
- [TypeScript Definitions](#typescript-definitions)
- [Task Actor Types](#task-actor-types)
- [Trigger Actor Types](#trigger-actor-types)
- [Common Actor Structure](#common-actor-structure)
- [Actor Naming Conventions](#actor-naming-conventions)
- [Configuration Interpolation Order](#configuration-interpolation-order)
- [BorgIQ Expressions](#borgiq-expressions)
- [Context Variables](#context-variables)
- [Q-lib Functions](#q-lib-functions)
- [Actor Source Files](#actor-source-files)
- [Actor Memory](#actor-memory)
- [Authentication](#authentication)
- [Actor ID, Validation, and Post-Processing](#actor-id-validation-and-post-processing)
- [Generation Instructions](#generation-instructions)
- [Workflow Composition](#workflow-composition)
- [Actor Connections and Edges](#actor-connections-and-edges)
- [Workflow Examples](#workflow-examples)
- [Workflow Patterns](#workflow-patterns)
  - [Web apps and forms — handed off to spokes](#web-apps-and-forms--handed-off-to-spokes)
  - [Collection migrations and provisioning](#collection-migrations-and-provisioning)
  - [Streams: events in order, consumed by cursor](#streams-events-in-order-consumed-by-cursor)
- [Editing Existing Workflows](#editing-existing-workflows)
- [Migration from Other Platforms](#migration-from-other-platforms)
- [Deploying and Testing with the CLI](#deploying-and-testing-with-the-cli)
  - [Canvas Bundles](references/cli/canvas-bundles.md)
  - [CLI Command Reference](references/cli/cli-command-reference.md)
  - [CLI Data Formats](references/cli/cli-data-formats.md)
  - [CLI Scaffolding Scripts](references/cli/cli-setup-scripts.md)
  - [CLI Troubleshooting](references/cli/cli-troubleshooting.md)

## 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:
```json
{
  "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](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.

| 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/](references/typescript/). Use these to understand exact data structures, validation rules, and type constraints.

| Reference File | Description |
|-------------|-------------|
| [actor-schemas-triggers.md](references/typescript/actor-schemas-triggers.md) | Trigger actor options and results (Button, Webhook, Email, Interface, App, Scheduled, Universal, Callable) |
| [actor-schemas-task-core.md](references/typescript/actor-schemas-task-core.md) | Core task actor schemas (AiActor, AiAgentActor, DenoActor, PythonActor, RouterActor, etc.) |
| [actor-schemas-task-http.md](references/typescript/actor-schemas-task-http.md) | HttpRequestActor options and authentication types |
| [actor-schemas-task-datastore.md](references/typescript/actor-schemas-task-datastore.md) | DataStoreActor actions (legacy — kept for TypeScript type reference) |
| [actor-schemas-task-collection.md](references/typescript/actor-schemas-task-collection.md) | CollectionActor actions (query, getItem, putItem, batchGet, batchWrite, etc.) |
| [actor-schemas-task-stream.md](references/typescript/actor-schemas-task-stream.md) | StreamActor actions (createStream, appendData, readStream, getStreamInfo, etc.) |
| [actor-schemas-task-messageprocessor.md](references/typescript/actor-schemas-task-messageprocessor.md) | MessageProcessorActor actions (inject, split, collect, fork, forkJoin, delay, etc.) |
| [actor-schemas-comment.md](references/typescript/actor-schemas-comment.md) | CommentActor schema |
| [form-components.md](references/typescript/form-components.md) | Interface form component schemas (InterfaceTriggerActor and InterfaceActor only, not used by AppTriggerActor) |
| [schemas.md](references/typescript/schemas.md) | Common schemas (IDs, files, runtime types, context, signals) |
| [common-types.md](references/typescript/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](references/http-request-actor.md) |
| **DenoActor** | Executes custom TypeScript/JavaScript code in a sandboxed Deno runtime | [deno-actor.md](references/deno-actor.md) |
| **PythonActor** | Executes custom Python code in a sandboxed Python runtime with UV package management | [python-actor.md](references/python-actor.md) |
| **AiActor** | Invokes AI models (LLMs) for text generation, structured output, and AI-powered tasks | [ai-actor.md](references/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](references/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](references/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](references/agent-harness-actor.md) |
| **AiRouterActor** | Routes messages to different outputs based on AI-powered classification | [ai-router-actor.md](references/ai-router-actor.md) |
| **RouterActor** | Routes messages based on boolean conditions (if/else, switch logic) | [router-actor.md](references/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](references/message-processor-actor.md) |
| **WebhookResponseActor** | Sends custom HTTP responses back to WebhookTriggerActor callers | [webhook-response-actor.md](references/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](references/callable-response-actor.md) |
| **CallFlowActor** | Invokes sub-flows by calling a CallableTriggerActor in another canvas/workspace | [call-flow-actor.md](references/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](references/interface-actor.md) |
| **SendEmailActor** | Sends text/HTML emails with optional attachments | [send-email-actor.md](references/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](references/collection-api.md#single-collection-design)). | [collection-actor.md](references/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](references/stream-api.md#collections-vs-streams)). | [stream-actor.md](references/stream-actor.md) |
| **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) |
| **CommentActor** | Non-functional UI element for adding notes, TODOs, and documentation to workflows | [comment-actor.md](references/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.**
>
> ```bash
> 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](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).

### 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](references/collection-api.md#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](references/button-trigger-actor.md) |
| **WebhookTriggerActor** | Receives HTTP requests at a unique webhook URL | [webhook-trigger-actor.md](references/webhook-trigger-actor.md) |
| **EmailTriggerActor** | Receives emails at a unique email address | [email-trigger-actor.md](references/email-trigger-actor.md) |
| **InterfaceTriggerActor** | Displays a web form and triggers on submission | [interface-trigger-actor.md](references/interface-trigger-actor.md) |
| **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) |
| **ScheduledTriggerActor** | Runs on a cron-based schedule | [scheduled-trigger-actor.md](references/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](references/universal-trigger-actor.md) |
| **CallableTriggerActor** | Invoked by parent flows (sub-flow entry point) | [callable-trigger-actor.md](references/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](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)).

### 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](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](references/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.

```yaml
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:

1. **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.

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

3. **options** — Actor-specific configuration. Has access to `inputs`, `vars`, `msg`, `ctx`, and `err`.

4. **Actor executes** — Results are stored in `results`.

5. **error** — Error handling. Has access to `results`. Determines if the actor failed and whether to retry.

6. **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):**
```yaml
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:**
```yaml
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):**
```yaml
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](references/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:**
```yaml
url: https://api.example.com/users/${{ inputs.userId }}
body: ${{ Q.toJSON(inputs.data) }}
data: ${{ msg.previous_actor.body }}
```

## Context Variables

See [references/context.md](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](references/error-handling.md).

## Q-lib Functions

Access utility functions via `Q.*`. See [references/q-lib.md](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**.

```yaml
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](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)).
- **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](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)
