# Chat Module

## Summary
A platform-level AI chat assistant that lets users interact with an LLM to manage their Activepieces projects through natural language. The chat connects to the platform's configured AI provider, streams responses via a custom WebSocket chunk reducer, and exposes Activepieces resources (flows, tables, connections, runs) as callable tools through the project's MCP server. Conversations are persisted per-user with support for message compaction, file attachments, multi-project context switching, two-phase (discovery/build) tool gating, and an action-preview gate for ad-hoc write actions. The full tool-call/tool-result history of every turn is persisted (all AI-SDK steps, not just the last), so the agent remembers what it already did within a conversation and does not re-run tools. Inputs are gathered conversationally through connection pickers and multi-question cards during discovery; once understood, the agent builds directly with no separate approval step (flow construction and publishing are not gated; a live test of a flow that contains write/destructive steps is gated behind a confirmation).

## Developer Quickstart

**Execution model (read this first).** The chat LLM loop runs in the **worker**, not the API. Send-message path: `chat-controller.ts` (`POST /conversations/:id/messages`) enqueues a `WorkerJobType.EXECUTE_CHAT_AGENT` job → worker `execute-chat-agent.ts` calls the `getChatConfig` RPC, assembles tools, and runs `run-chat-turn.ts` (the shared `streamText()` DI loop) → chunks stream back via the `sendChatEvent` RPC → websocket `CHAT_MESSAGE_CHUNK` (filtered by `runId`) → frontend `use-streaming-reducer.ts` + `chunk-reducer.ts`. `chat-service.ts` only does conversation CRUD + persistence.

**Run it locally (EE + chat).** Chat is EE/Cloud-only and refuses to run on the default PGLite dev DB. To bring it up self-hosted:
- `AP_EDITION=ee` **and** `AP_DB_TYPE=POSTGRES` (+ `AP_POSTGRES_*`) — set in `.env.dev` (Turbo strips ad-hoc env vars unless declared in `globalPassThroughEnv`). Redis is also required.
- A platform only exists in dev via the **dev seed** (`dev-seeds.ts`, `dev@ap.com` / `12345678`), which is gated off for EE — temporarily allow it, or seed under CE first, then switch to EE on the same Postgres DB.
- `platform.plan.chatEnabled` must be true. In CE the plan is derived live from `OPEN_SOURCE_PLAN`; in EE it's a DB row in `platform_plan` (set by a license key via Platform Admin → Setup → License Keys, or flip the flags directly for local testing).

**Debugging a chat run (local, full logging).** Given a chat (conversation) id, reconstruct the whole turn — requests, responses, the worker LLM loop, every tool call's full input/output, websocket chunks, and what the user saw — from one NDJSON corpus. Dev-only, off by default; never enabled in cloud/prod.
- Turn it on: backend `LOG_FILE=true` (api) + `AP_LOG_FILE=true` (worker) + `AP_LOG_LEVEL=debug`/`LOG_LEVEL=debug` (debug captures per-chunk, per-step, and full tool I/O); frontend `localStorage['chat-debug']='1'` then reload the chat page.
- Where logs land: repo-root `.evlog/logs/<date>.jsonl` (gitignored). The evlog filesystem drain (`evlog/fs`, wired in `packages/server/utils/src/evlog-drains.ts`) collects **both** api and worker events; the browser ships its events to the dev-only `POST /v1/logs/client` ingest (`packages/server/api/src/app/helper/logs/`, registered in `app.ts` only when `LOG_FILE=true` and edition ≠ cloud), which re-emits them into the same file tagged `source:"client"`.
- Correlation: every chat log line carries `conversation: { id }` + `run: { id }` (the per-message run id, threaded controller → job → worker → RPC; `worker.ts` maps chat `runId` to `run.id`, not `flowRun.id`). The frontend logger (`packages/web/src/lib/chat-debug-logger.ts`) tags the same ids.
- Read it: `npm run chat:logs -- <conversationId> [runId]` (`scripts/chat-logs.mjs`) merges api/worker/web events and prints a timeline; or use the `analyze-logs` skill; or `jq -c 'select(.conversation.id=="X")' .evlog/logs/*.jsonl | jq -s 'sort_by(.timestamp)'`.

**Extension points (where to edit).**
| Goal | Edit |
| --- | --- |
| Add a local `ap_*` tool | `worker/.../ee/chat/chat-worker-tools.ts` (tool def/factory) + `ee/chat/tools/chat-tools.ts` (server-side logic: connections, RPC) |
| Change the system prompt / guides | `src/assets/prompts/*.md` (loaded by `prompt/chat-prompt.ts`) |
| Change tool gating / phases | `core/shared/src/lib/ee/chat/tool-phases.ts` (`BUILD_ONLY_TOOL_NAMES`, `CHAT_HIDDEN_TOOL_NAMES`) |
| Change write/approval classification | `core/shared/src/lib/ee/chat/tool-classification.ts` |
| Change the streaming loop behavior | `worker/.../ee/chat/run-chat-turn.ts` |
| Add an endpoint | `ee/chat/chat-controller.ts` |

## Key Files
- `packages/server/api/src/app/ee/chat/chat.module.ts` — module registration; gates `/v1/chat` with `chatVisibilityGuard` (per-user visibility, not just the `chatEnabled` flag)
- `packages/server/api/src/app/ee/chat/chat-visibility-helper.ts` — `chatVisibilityGuard` + `resolveChatEnabledForUser` (edition + embed + rollout/grandfather); also used by the platform endpoint to surface the effective `plan.chatEnabled`
- `packages/server/api/src/app/ee/chat/chat-rollout-service.ts` — cloud rollout cohort: `isRolloutOpen` (cached count vs cap), `hasUserChatted` (grandfather), `recordLanding`/`recordChatted` (deduped upserts), `getFunnelAggregate`
- `packages/server/api/src/app/ee/chat/chat-rollout-user-entity.ts` — `chat_rollout_user` table (one row per distinct cloud user; landedAt/chattedAt)
- `packages/server/api/src/app/ee/chat/chat-funnel-sync.ts` — cloud-only fire-and-forget push of the funnel aggregate to console.activepieces.com (auth: `CONSOLE_API_SECRET_KEY`)
- `packages/server/api/src/app/ee/chat/chat-funnel-tracking-module.ts` — schedules the funnel-sync system job (every 30 min)
- `packages/core/shared/src/lib/ee/chat/chat-visibility.ts` — pure `resolveChatEnabled` (CE never / EE flag / embed never / Cloud `flag || rolloutOpen || userHasChatted`)
- `packages/server/api/src/app/ee/chat/chat-controller.ts` — HTTP endpoints (conversations CRUD, messages, tool approvals)
- `packages/server/api/src/app/ee/chat/chat-eval-controller.ts` — admin eval/playground endpoints: prompt-source inspection, batch dry-run simulate, and the interactive single-turn eval (stateful `turn/start` + state-poll) backing the console's live prompt playground
- `packages/server/worker/src/lib/execute/jobs/ee/chat/run-chat-turn.ts` — pure dependency-injected streaming-loop core shared by the production worker, the replay eval gate, and the live playground
- `packages/server/api/src/app/ee/chat/chat-service.ts` — core business logic (conversation management, message streaming)
- `packages/server/api/src/app/ee/chat/chat-conversation-entity.ts` — ChatConversation TypeORM entity
- `packages/server/api/src/app/ee/chat/chat-helpers.ts` — provider/tier resolution, project access, conversation fetch/lock
- `packages/server/api/src/app/ee/chat/chat-history-hygiene.ts` — collapses stale tool outputs in history to control context dilution
- `packages/core/shared/src/lib/ee/chat/tool-phases.ts` — two-phase (discovery/build) denylist-based tool gating; shared by API and worker
- `packages/server/api/src/app/ee/chat/chat-model-factory.ts` — creates AI SDK `LanguageModel` from provider config (OpenAI, Anthropic, Google, Azure, Bedrock, Cloudflare, Custom)
- `packages/server/api/src/app/ee/chat/chat-compaction.ts` — long-conversation context management via summarization
- `packages/server/api/src/app/ee/chat/chat-approval-gate.ts` — Redis pub/sub gate for tool execution approval (5-min timeout); uses atomic SET NX for first-decision-wins semantics; also stores per-conversation cancel signals with a 10-min TTL; manages server-side connection store (available/selected connections per conversation+piece) and pending gate persistence for refresh resilience
- `packages/server/api/src/app/ee/chat/chat-file-utils.ts` — file attachment processing (base64, MIME validation, 10MB limit)
- `packages/server/api/src/app/ee/chat/tools/chat-tools.ts` — cross-project tool execution (auth discovery, action execution with server-managed connections, resource listing); `ap_execute_action` auto-fills connectionExternalId and projectId from Redis connection store — the LLM never sees credential IDs
- `packages/core/shared/src/lib/ee/chat/tool-classification.ts` — consolidated tool classification predicates (approval-required, action preview risk, read/write action detection) as single source of truth; imported by both API server and worker
- `packages/server/api/src/app/ee/chat/mcp/chat-mcp.ts` — connects to Activepieces MCP server for project-scoped tools with approval wrapping
- `packages/server/api/src/app/ee/chat/history/chat-history.ts` — reconstructs chat history from AI SDK `ModelMessage` format
- `packages/server/api/src/app/ee/chat/prompt/chat-prompt.ts` — builds system prompt from markdown templates in `src/assets/prompts/`
- `packages/server/api/src/app/ee/chat/chat-sync-job.ts` — exposes `chatAnalyticsTelemetry` with two fire-and-forget paths (cloud-only): `sendConversationUpdate` syncs the full conversation to console.activepieces.com (grouped by platform, authenticated with each platform's license key from `platform_plan` as a Bearer token; platforms without a license key are skipped), and `sendMessageBillingEvent` emits a PostHog `chat_message` billing event (`BillingEvents.CHAT_MESSAGE`, keyed by license key as `distinctId`, with `provider`/`model`/`toolsUsed` from the latest turn). Also exposes `chatAnalyticsBulkSync` for admin bulk sync; falls back to reconstructing messages from raw ModelMessage[] when uiMessages is null
- `packages/core/shared/src/lib/ee/chat/index.ts` — shared Zod schemas, types (ChatConversation, request DTOs, ChatHistoryMessage), typed tool outputs (`ChatToolOutputs`); includes `PersistedActionReceiptPartSchema` for persisting action receipts, and `PersistedSourceUrlPartSchema` / `PersistedSourceDocumentPartSchema` for persisting web-search citations, in conversation history; `PersistedToolCallPartSchema` includes optional `title` and `description` fields for UI chip label and conversational status text
- `packages/web/src/app/routes/chat-with-ai/index.tsx` — main chat page component
- `packages/web/src/app/routes/chat-with-ai/ai-chat-box.tsx` — chat interface with provider check, message streaming, Zustand store provider; manages suggestion prefill via counter-based key remount on empty-state suggestion clicks
- `packages/web/src/app/routes/chat-with-ai/conversation-list.tsx` — conversation history sidebar
- `packages/web/src/app/routes/chat-with-ai/components/` — sub-components (input, assistant message, user message, thinking details panel, approval forms, connection picker, multi-question card, action-preview-card, action-receipt-card); `chat-card-primitives.tsx` provides the shared card primitives (`ChatCard`, `ChatCardHeader`, `ChatOptionRow`, `ChatOptionBadge`, `ChatAnswerInputRow`, `AnsweredQuestionsCard`, `ChatConfirmationBubble`, `ChatCardSkeleton`) reused by the project picker and the multi-question form; `chat-empty-state.tsx` renders a personalized greeting with the user's first name, horizontal flow cards with images, and a vertical text-suggestion list with lazy-loaded icons; `tool-shimmer-pills.tsx` renders the in-progress tool chip — a slot-machine logo strip (`search-apps-animation.tsx`) for `ap_research_pieces`, and a generic shimmer pill for every other tool
- `packages/web/src/features/chat/lib/chat-api.ts` — API client for `/v1/chat/*` endpoints
- `packages/web/src/features/chat/lib/chat-store.ts` — Zustand store for interaction state (approvals, plan progress, display cards, thinking panel); `selectActiveDisplayTool` also matches the `input-streaming` state so a `ChatCardSkeleton` paints as soon as a display tool starts streaming, then swaps to the real card at `input-available`
- `packages/web/src/features/chat/lib/chat-store-context.tsx` — React context provider and `useChatStoreContext` selector hook
- `packages/web/src/features/chat/lib/use-chat.ts` — `useAgentChat()` hook managing message state (persisted, optimistic, streaming); stale-check only reconciles when the server reports the conversation is no longer STREAMING; exposes `isAwaitingResponse` (true while `awaiting-stream`/`streaming`/`submitting`, drops the polling/reconcile tail) for quick-reply reveal timing
- `packages/web/src/features/chat/lib/chunk-reducer.ts` — pure streaming state machine that accumulates `UIMessageChunk` events into a `ChatUIMessage`
- `packages/web/src/features/chat/lib/use-streaming-reducer.ts` — WebSocket-driven streaming lifecycle hook; buffers chunks and throttles React re-renders
- `packages/web/src/features/chat/lib/chat-types.ts` — frontend type definitions, tool output parsing, display/hidden tool name sets, `CreditsWarning` type
- `packages/web/src/features/chat/lib/use-credits-state.ts` — `useCreditsState()` hook computing credits warning/exhaustion state from platform usage and AI provider config
- `packages/web/src/app/routes/chat-with-ai/components/credits-banner.tsx` — amber/red banner shown when AI credits reach warning threshold (>=70%) or are exhausted
- `packages/web/src/features/chat/lib/use-voice-input.ts` — `useVoiceInput()` hook for speech-to-text via the Web Speech API (`SpeechRecognition`)
- `packages/web/src/features/chat/lib/use-tts.ts` — `useTts()` hook for text-to-speech via the `SpeechSynthesis` API
- `packages/web/src/features/chat/components/voice-waveform.tsx` — animated waveform bars shown on the stop-recording button

## Edition Availability
- Community (CE): not available (module not registered)
- Enterprise (EE): available when `platform.plan.chatEnabled` is true; the eval/playground endpoints (`/v1/chat/eval/*`) are internal global-API-key dry-runs and do **not** require `chatEnabled` (they run as the platform owner with tools disabled)
- Cloud: shown to all normal (non-embed) cloud users during a capped beta **without** requiring `chatEnabled` — see **Cloud rollout cap**. Embedded cloud sessions never show chat; a platform that has `chatEnabled` (e.g. cloud enterprise) always keeps it

## Domain Terms
- **ChatConversation** — a persisted conversation between a user and the AI assistant, scoped to a platform and user; optionally scoped to a project for tool access
- **Message compaction** — when a conversation exceeds a token threshold, older messages are summarized by the LLM and replaced with a summary to keep context within the model's window
- **Tool approval gate** — a Redis pub/sub mechanism that blocks on user input until the user responds in the UI; times out after 5 minutes. Used to wait on display-tool cards (connection picker, multi-question card, project picker), the ad-hoc action-preview gate, and the test-flow write gate. Flow build and publish and most MCP tools are NOT gated; `ap_test_flow` is gated only when the flow contains a write/destructive step (see **Write-check gate**)
- **Two-phase toolset gating** — the agent runs in a `discovery` or `build` phase (`tool-phases.ts`); a denylist hides build-only tools during discovery to shrink the tool surface. `ap_set_phase` flips the phase; the gate auto-widens if a build/manage tool fires so the agent can't get stuck. The Tables write tools (`ap_create_table`, `ap_insert_records`, `ap_update_record`, `ap_delete_records`, `ap_manage_fields`, `ap_delete_table`) are available to the chat agent (build-only, so surfaced in the build phase) — they were previously hidden via `CHAT_HIDDEN_TOOL_NAMES` but had no chat equivalent, leaving the agent unable to create or write tables
- **Local tools** — chat-specific tools not part of MCP: `ap_set_session_title`, `ap_select_project`, `ap_deselect_project`, `ap_execute_action`, `ap_list_across_projects`, `ap_explore_data`, `ap_load_guide`, `ap_set_phase`, `ap_fetch_url`
- **Web access** — provider-native web search combined with the uniform `ap_fetch_url` fetch tool, on whenever chat is enabled. Search rides on the configured LLM credential (no second BYOK): OpenRouter/Activepieces use the `web` plugin (wired at model creation), Anthropic uses `web_search_20250305`, Google uses Search grounding; OpenAI and other providers degrade to no search (a runtime note in the system prompt tells the agent), but `ap_fetch_url` still works everywhere. Both are disabled during dry runs. Results emit as `SOURCE_URL`/`SOURCE_DOCUMENT` chat parts, persist in history, and render as `<Source>` citation pills after streaming completes. Provider capability lives in `supportsWebSearch` / `buildWebSearchTools` (`chat-ai-utils.ts`)
- **Display tools** — tools that render interactive UI cards: `ap_show_connection_required`, `ap_show_connection_picker`, `ap_show_project_picker`, `ap_show_questions`, `ap_show_quick_replies`
- **MCP tools** — project-scoped tools loaded from the Activepieces MCP server when a project is selected; wrapped only with a per-call execution timeout (`withToolTimeouts`) — the chat no longer gates them behind approval
- **Tool call UX metadata** — optional `title` (2-4 word chip label) and `description` (first-person conversational sentence) stored on `PersistedToolCallPart`; description is sourced from the preceding `ap_update_thinking_status` text with `input.description` as fallback, rendered above the tool card chip
- **Server-managed connections** — connection externalIds are never exposed to the LLM; `ap_discover_action_auth` stores available connections in Redis (with `grantedScopes` and `requiredScopes` for scope-aware selection), `ap_show_connection_picker` stores the user's selection, and `ap_execute_action` auto-fills the connection from the store
- **Action preview gate** — write/destructive actions show a preview card (parameters, connection) before execution; classification uses a hybrid AI-driven (`needsConfirmation` flag) + server hard-floor (name pattern matching) approach; persisted as pending gate in Redis for refresh resilience
- **Write-check gate** — before `ap_test_flow` runs a live test, the worker (`wrapTestFlowGate` in `chat-worker-tools.ts`) calls the internal `__flow_write_check` RPC, which loads the flow (scoped to the conversation's project) and flags any PIECE step whose `actionName` matches a write pattern (`chatToolClassification.isWriteActionName`). If the flow has write steps a confirmation card is shown (stored as a pending gate for refresh resilience) and declining cancels the live run; read-only flows run with no gate, and the gate fails open if the RPC errors
- **Action receipt** — server-rendered card showing action result (status, output, connection badge, timestamp); emitted as `ACTION_RECEIPT` event and persisted as `PersistedChatPartType.ACTION_RECEIPT` in conversation history; receipt cards visually split the thinking accordion so each action's thinking section appears immediately above its receipt card
- **Pending gate persistence** — when a display tool or action preview blocks on approval, gate metadata is stored in Redis so the frontend can re-show the card after page refresh; cleared automatically when the gate resolves
- **Stream reconnection** — when loading a STREAMING conversation (e.g. after refresh), the frontend extracts the last assistant message from history as the streaming message's initial content, calls `getPendingGate` to inject synthetic display-tool parts for pending gates, then calls `startStream`; a socket `connect` handler re-registers the chunk listener and resets the stale-check timer on reconnect; the periodic stale-check calls `GET /conversations/:id` and only reconciles when the server's status is no longer STREAMING, so long tool executions (>15s) are not incorrectly torn down
- **Run ID event filtering** — each agent run gets a unique `runId` (generated in the controller, included in job data, threaded through all websocket events); the frontend's event handler filters by `runId` to prevent stale FINISHED/ERROR events from old runs from killing new streams; combined with a generation counter that guards against stale async reconcile callbacks
- **Eval conversation scoping** — conversations created by the interactive eval (`turn/start`) carry a reserved id prefix (`evalconv`, within the 21-char id column; see `isEvalConversationId` in `chat-helpers.ts`). The eval continue/`state` endpoints refuse any id without that prefix, so the global eval API key cannot read or mutate arbitrary (real) conversations. Because eval conversations are owned by the platform owner, the regular chat path also excludes them: `listConversations` filters out the prefix and `chatService.getConversationOrThrow` rejects eval ids — so the owner never sees them in their chat list and can't message one through the normal (non-dry-run) flow and trigger real tools. The eval worker is unaffected because it loads the conversation via `chatHelpers.getConversationOrThrow` directly
- **AI provider** — a platform-configured LLM provider with an `enabledForChat` flag; the chat resolves the first enabled provider and its default model
- **Streaming cancel** — cancel keys are run-scoped (`chat-cancel:{conversationId}:{runId}`) so each worker only checks its own key; a 3-second periodic timer polls the Redis key so cancellation fires within 3 seconds regardless of step boundaries; partial messages (from completed steps via `onAbort` callback) are saved to preserve context for resume; when a new message arrives while STREAMING, the controller reads the active runId, cancels that specific run, and resets status to IDLE before queuing the new job
- **Stale recovery** — when `getConversationOrThrow` fetches a conversation stuck in STREAMING for more than 2 minutes, it automatically resets the status to IDLE before returning
- **Project context** — the currently selected project for a conversation; determines which MCP tools are available and scopes resource access
- **Chat tiers** — model configurations (fast/smart/premium) with different thinking budgets; displayed as Fast/Expert/Heavy in the UI with per-tier descriptions. The tier's full thinking budget applies only in the `build` phase; discovery/handoff turns (pickers, questions, confirmations) use a small fixed budget (`DISCOVERY_THINKING_BUDGET` in `run-chat-turn.ts`) so interactive cards and replies appear quickly. When a turn transitions to `build` mid-stream, the discovery stream is stopped and resumed in a fresh `streamText` call at the full budget, so build and one-time-task reasoning is never under-resourced
- **Credits warning banner** — a dismissible amber banner shown when Activepieces AI credits usage reaches 70%; a non-dismissible red banner is shown when credits are fully exhausted
- **Cloud rollout cap** — on cloud, chat opens to all non-embed users without the `chatEnabled` flag until **200 distinct users have sent a message** (`CLOUD_CHAT_ROLLOUT_CAP`, overridable via `AP_CLOUD_CHAT_ROLLOUT_CAP`). Enforced cloud-local as a single global cohort counted in `chat_rollout_user`; monotonic (closed state cached in Redis); visibility decided per-user by `chatVisibility.resolveChatEnabled`
- **Grandfathered users** — once the cap closes, users who already chatted keep chat (`hasUserChatted`); new cloud users no longer see it
- **Chat funnel telemetry** — distinct users who landed on the chat page (`recordLanding`) vs. who sent a message (`recordChatted`), plus progress toward the cap; a cloud-only scheduled job (`chat-funnel-sync`) pushes the `{ landed, chatted, cap, closed }` aggregate to console.activepieces.com (separate from the PostHog billing event)

## Data Model

**ChatConversation**: id, platformId, userId, projectId (nullable), title (nullable), modelName (nullable), messages (JSONB array of `ModelMessage`), summary (text, nullable — compaction summary), summarizedUpToIndex (int, nullable — index up to which messages are summarized).
- Relations: platform (many-to-one), project (many-to-one, SET NULL on delete), user (many-to-one, CASCADE on delete)
- Index: `idx_chat_conversation_platform_user_created_id` on (platformId, userId, created, id)

**ChatRolloutUser** (`chat_rollout_user`, cloud rollout/funnel): id, userId (unique), platformId, landedAt (nullable), chattedAt (nullable).
- Partial indexes on `chattedAt` and `landedAt` (WHERE NOT NULL) for cheap cohort/funnel counts; FKs to user + platform (CASCADE on delete).
- Rows with `chattedAt IS NOT NULL` are the rollout cohort (capped at 200). The cap/funnel `COUNT`s are **intentionally global** (not tenant-scoped) — a single cloud-wide cohort.

## Key Service Methods
- `createConversation()` — creates a new conversation for a user on a platform; accepts an optional explicit `id` (defaults to `apId()`) so the interactive eval can mint a reserved-prefix conversation id
- `listConversations()` — cursor-paginated list of user's conversations, ordered by creation date descending; excludes messages, uiMessages, and summary columns for performance
- `getConversationOrThrow()` — fetches a conversation, enforcing ownership (platformId + userId); auto-recovers stale STREAMING conversations to IDLE after a 2-minute timeout
- `updateConversation()` — updates title and/or modelName
- `deleteConversation()` — deletes a conversation after ownership check; blocked while status is STREAMING
- `getMessages()` — reconstructs `ChatHistoryMessage[]` from stored `ModelMessage[]`
- Sending a message does **not** stream inline in `chat-service.ts` anymore. The controller (`chat-controller.ts`) enqueues a `WorkerJobType.EXECUTE_CHAT_AGENT` job (with `runId`) and returns immediately; the **worker** runs the streaming loop. `chat-service.ts` now owns conversation CRUD + persistence (`saveChatMessages` RPC), not the LLM loop. See **Execution model** in Developer Quickstart.

## Local Tools
- `ap_set_session_title` — auto-names the conversation after the first exchange
- `ap_select_project` — switches project context (scopes MCP tools to that project)
- `ap_execute_action` — executes a single piece action ad-hoc (e.g. "check my inbox"); connections are managed server-side (the LLM never sees externalIds); write/destructive actions trigger an action preview gate before execution; emits `ACTION_RECEIPT` events after completion
- `ap_list_across_projects` — lists flows, tables, runs, or connections across all user-accessible projects
- `ap_deselect_project` — clears the selected project context
- `ap_explore_data` — read-only exploration of the user's data (sheets, channels, columns) to build understanding during discovery; never configures the automation
- `ap_load_guide` — loads an on-demand prompt guide (`build_flow`, `one_time_task`, `error_handling`, `http_fallback`, `control_flow`, `state`, `tables`, `ai`) so guidance is only in context when needed
- `ap_fetch_url` — SSRF-safe read-only GET of any public http(s) URL (`safeHttp.axios`); HTML is stripped to text via `string-strip-html` and large results are truncated. Available in both phases on every provider (disabled in dry runs); built in `createWebTools` (`chat-worker-tools.ts`)
- `ap_set_phase` — flips the agent between the `discovery` and `build` tool phases

## Display Tools
- `ap_show_connection_required` — prompts the user to connect a service
- `ap_show_connection_picker` — lets the user choose between multiple connections; no longer receives connections array from LLM (server-managed); returns only `{ selected: true, label }` to LLM, stripping externalIds
- `ap_show_project_picker` — lets the user select a project; accepts an optional `question` to title the card; renders up to 3 suggested-project rows plus a "Show all N projects" row that opens a searchable 2-column grid, and a resolved pick renders as an `AnsweredQuestionsCard` user-message bubble
- `ap_show_questions` — renders an interactive multi-question form; after the user submits, the answers recap is rendered as a right-aligned user-message bubble (matching the `UserMessage` style) rather than a bordered card
- `ap_show_quick_replies` — shows up to 3 suggested follow-ups as a stacked list docked above the chat input within the message flow (via `mt-auto`, so they dock above the input when at the bottom and scroll away when scrolling up; hidden during blocking cards and while the answer is streaming); revealed as soon as text streaming completes rather than waiting for the post-stream reconcile tail; emitted only when concrete, relevant next steps exist. Optional `offerRecurringAutomation` flag (set after a successful one-time task) pins a special "Run this automatically every day" chip (`recurring-chip.tsx`, gradient label + animated icon) as the last suggestion; clicking it sends `RECURRING_AUTOMATION_REPLY` verbatim, which the prompt guides convert into a daily recurring automation

## Endpoints
- `POST /v1/chat/conversations` — create conversation
- `GET /v1/chat/conversations` — list conversations (cursor, limit)
- `GET /v1/chat/conversations/:id` — get conversation
- `POST /v1/chat/conversations/:id` — update conversation (title, modelName)
- `DELETE /v1/chat/conversations/:id` — delete conversation
- `GET /v1/chat/conversations/:id/messages` — get conversation messages
- `POST /v1/chat/conversations/:id/messages` — send message; if the conversation is STREAMING the old run is cancelled; response body is `{ conversationId, runId }` — clients use runId to filter stale events
- `POST /v1/chat/tool-approvals/:gateId` — approve or deny a tool execution
- `POST /v1/chat/conversations/:id/cancel` — cancel an in-progress streaming response
- `POST /v1/chat/funnel/landing` — record that the user landed on the chat page (cloud rollout funnel; deduped per user, no-op off cloud)
- `GET /v1/chat/conversations/:id/connections?pieceName=` — get available connections for connection picker; falls back to `findConnectionsForPiece` when the Redis cache is empty and stores the result for future calls
- `GET /v1/chat/conversations/:id/pending-gate` — get pending approval gate for refresh resilience (returns gate info so the frontend can re-show display tool cards)
- `GET /v1/chat/eval/prompt-sources` — returns the raw prompt template sources (core + project-context + on-demand guides); requires the global eval API key
- `POST /v1/chat/eval/simulate` — replays one or more user turns (`userMessages[]`, or legacy single `userMessage`) sequentially in one ephemeral conversation with an optional prompt override in dry-run mode (tools are not executed, no MCP, runs as the platform owner — no side effects, no sandbox), polls until each turn settles, and returns the full transcript synchronously (`status` is `IDLE`/`ERROR`/`TIMEOUT`); the `promptOverride.guides` is shallow-merged over the defaults so a partial override keeps the untouched guides; requires the global eval API key
- `GET /v1/chat/eval/sandbox-platform` — returns `{ platformId }` for the oldest platform on the instance, so a caller replaying a foreign conversation (e.g. the console in dev, whose conversations reference a cloud platform absent locally) can resolve a local platform to sandbox the dry-run in; requires the global eval API key
- `POST /v1/chat/eval/turn/start` — interactive eval: create-or-continue a stateful eval conversation and enqueue ONE dry-run agent turn, returning immediately (`{ conversationId, runId, priorAssistantTurns }`); the caller polls the state endpoint to stream progress (the worker persists `uiMessages` per step). New conversations are created with a reserved id prefix (see **Eval conversation scoping**); continuation rejects a conversation that is already STREAMING (409, no racing two workers on one row) and any id not created by the eval flow (404); requires the global eval API key
- `GET /v1/chat/eval/conversations/:id/state` — pure observer that reads the row directly (not `getConversationOrThrow`, which would reset a stale STREAMING row to IDLE) and returns `{ status, uiMessages }`; rejects non-eval conversation ids (404); requires the global eval API key

- `POST /v1/admin/chat/sync-all` — bulk historical sync of all conversations to console analytics (admin API key required)

All chat endpoints require `PrincipalType.USER` authentication at the platform level. The admin sync endpoint uses `api-key` header auth.

## Message Flow
1. User sends message via `POST /conversations/:id/messages`
1a. If the conversation is STREAMING, the controller reads the active runId, cancels that run, and resets status to IDLE
1b. If the platform's chat provider is ACTIVEPIECES and `usageRemaining <= 0`, the endpoint returns a 402 `AI_CREDIT_LIMIT_EXCEEDED` error before queuing the job; the frontend surfaces a non-dismissible error banner
1c. Controller generates a `runId`, includes it in the job data, and returns it to the frontend
1d. Controller enqueues a `WorkerJobType.EXECUTE_CHAT_AGENT` job and returns `{ conversationId, runId }`; all LLM work happens in the worker (`execute-chat-agent.ts`), not the API process
1e. On cloud, the controller records the user in the rollout/funnel cohort (`chatRolloutService.recordChatted`, deduped, fire-and-forget). The `/v1/chat` `chatVisibilityGuard` preHandler has already enforced per-user visibility (rollout open or grandfathered) before any of this
2. Worker calls the `getChatConfig` RPC (history, system prompt, resolved provider, MCP credentials, project list), assembles the tool set (local + display + cross-project + web + MCP, phase-gated), and connects the MCP client
3. If conversation is long, compaction summarizes older messages
4. `run-chat-turn.ts` runs `streamText()` (the shared DI loop) with `stopWhen: isLoopFinished()` (no hard step cap), `prepareStep` (narrows toolset to the current discovery/build phase), and `experimental_repairToolCall`; chunks stream back to the API via the `sendChatEvent` RPC, which emits `CHAT_MESSAGE_CHUNK` over websocket to the user's socket (filtered by `runId`)
5. Display-tool cards (connection picker, questions) and ad-hoc write actions (`ap_execute_action` previews) pause and emit a gate request to the UI via the stream; flow build and publish run without gating, and `ap_test_flow` is gated only when the flow has write steps (the worker's `__flow_write_check` RPC confirms before a live run)
6. User responds via `POST /tool-approvals/:gateId`, unblocking the gate via Redis pub/sub
7. On stream completion, assistant messages are appended to the stored conversation
8. On cloud, `chatAnalyticsTelemetry` pushes the updated conversation to `console.activepieces.com` for monitoring (fire-and-forget, authenticated with the platform's license key as a Bearer token and skipped when the platform has no license key); messages are sourced from uiMessages when available, falling back to reconstruction from raw ModelMessage[] for older conversations
9. Also on cloud, `chatAnalyticsTelemetry.sendMessageBillingEvent` emits a `chat_message` billing event to PostHog (keyed by the platform's license key, carrying provider/model/toolsUsed for the latest turn) for usage metering; skipped when the platform has no license key
