.agents/features/chat.md
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).
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.dev-seeds.ts, [email protected] / 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.
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..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".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.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 |
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.chatEnabledpackages/server/api/src/app/ee/chat/chat-rollout-service.ts — cloud rollout cohort: isRolloutOpen (cached count vs cap), hasUserChatted (grandfather), recordLanding/recordChatted (deduped upserts into the local chat_rollout_user table)packages/server/api/src/app/ee/chat/chat-rollout-user-entity.ts — chat_rollout_user table (one row per distinct cloud user; landedAt/chattedAt; chattedAt drives the cap)packages/server/api/src/app/ee/chat/chat-sync-job.ts — chatAnalyticsTelemetry: sendConversationUpdate (per-save conversation sync) + sendMessageBillingEvent + sendRolloutFunnelUpdate (pushes the landed/chatted/cap/closed funnel snapshot to console.activepieces.com); all real-time + cloud-only. Conversation sync and the funnel push authenticate with the instance CONSOLE_API_SECRET_KEY Bearer (no license key — free cloud users have none)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 playgroundpackages/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 playgroundpackages/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 entitypackages/server/api/src/app/ee/chat/chat-helpers.ts — provider/tier resolution, project access, conversation fetch/lockpackages/server/api/src/app/ee/chat/chat-history-hygiene.ts — collapses stale tool outputs in history to control context dilutionpackages/core/shared/src/lib/ee/chat/tool-phases.ts — two-phase (discovery/build) denylist-based tool gating; shared by API and workerpackages/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 summarizationpackages/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 resiliencepackages/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 IDspackages/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 workerpackages/server/api/src/app/ee/chat/mcp/chat-mcp.ts — connects to Activepieces MCP server for project-scoped tools with approval wrappingpackages/server/api/src/app/ee/chat/history/chat-history.ts — reconstructs chat history from AI SDK ModelMessage formatpackages/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 three fire-and-forget paths (cloud-only): sendConversationUpdate syncs the full conversation to console.activepieces.com — all cloud conversations are sent (licensed and free), authenticated once with the instance CONSOLE_API_SECRET_KEY as a Bearer token; each conversation carries its own platform license key in the payload (null for free platforms with no platform_plan.licenseKey), which console stamps onto the row. 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). sendRolloutFunnelUpdate pushes the rollout funnel snapshot (landed/chatted/cap/closed from chatRolloutService.getFunnelSnapshot) to console.activepieces.com/api/chat-analytics/external/rollout-funnel over the same CONSOLE_API_SECRET_KEY channel; triggered on chat-page landing and after every sent message. Also exposes chatAnalyticsBulkSync for admin bulk sync; falls back to reconstructing messages from raw ModelMessage[] when uiMessages is nullpackages/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 textpackages/web/src/app/routes/chat-with-ai/index.tsx — main chat page componentpackages/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 clickspackages/web/src/app/routes/chat-with-ai/conversation-list.tsx — conversation history sidebarpackages/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 toolpackages/web/src/features/chat/lib/chat-api.ts — API client for /v1/chat/* endpointspackages/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-availablepackages/web/src/features/chat/lib/chat-store-context.tsx — React context provider and useChatStoreContext selector hookpackages/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 timingpackages/web/src/features/chat/lib/chunk-reducer.ts — pure streaming state machine that accumulates UIMessageChunk events into a ChatUIMessagepackages/web/src/features/chat/lib/use-streaming-reducer.ts — WebSocket-driven streaming lifecycle hook; buffers chunks and throttles React re-renderspackages/web/src/features/chat/lib/chat-types.ts — frontend type definitions, tool output parsing, display/hidden tool name sets, CreditsWarning typepackages/web/src/features/chat/lib/use-credits-state.ts — useCreditsState() hook computing credits warning/exhaustion state from platform usage and AI provider configpackages/web/src/app/routes/chat-with-ai/components/credits-banner.tsx — amber/red banner shown when AI credits reach warning threshold (>=70%) or are exhaustedpackages/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 APIpackages/web/src/features/chat/components/voice-waveform.tsx — animated waveform bars shown on the stop-recording buttonplatform.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)chatEnabled — see Cloud rollout cap. Embedded cloud sessions never show chat; a platform that has chatEnabled (e.g. cloud enterprise) always keeps itCanonical term definitions live in the bounded-context glossaries — see CONTEXT-MAP.md.
ap_test_flow is gated only when the flow contains a write/destructive step (see Write-check gate)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 tablesap_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_urlap_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)ap_show_connection_required, ap_show_connection_picker, ap_show_project_picker, ap_show_questions, ap_show_quick_replieswithToolTimeouts) — the chat no longer gates them behind approvaltitle (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 chipap_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 storeneedsConfirmation flag) + server hard-floor (name pattern matching) approach; persisted as pending gate in Redis for refresh resilienceap_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 errorsACTION_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 cardgetPendingGate 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 downrunId (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 callbacksturn/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 directlyenabledForChat flag; the chat resolves the first enabled provider and its default modelchat-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 jobgetConversationOrThrow fetches a conversation stuck in STREAMING for more than 2 minutes, it automatically resets the status to IDLE before returningbuild 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-resourcedchatEnabled 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.resolveChatEnabledhasUserChatted); new cloud users no longer see itchat_rollout_user (cloud-only, deduped, no license key needed): recordChatted on first message sets chattedAt (drives the cap), recordLanding on chat-page open sets landedAt. Counts feed getFunnelSnapshot (landed/chatted/cap/closed), which is pushed to console.activepieces.com via sendRolloutFunnelUpdate — triggered by the /funnel/landing endpoint and after every sent message (recordChatted path). No scheduled job; the push is real-time and over the shared CONSOLE_API_SECRET_KEY channel.sendConversationUpdate ships all cloud chat conversations (licensed and free) to console.activepieces.com via sendConversationUpdate. Free-tier conversations arrive with license_key = null and are distinguishable there by a free-tier filter; licensed conversations carry their platform's real license key.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).
idx_chat_conversation_platform_user_created_id on (platformId, userId, created, id)ChatRolloutUser (chat_rollout_user, cloud rollout cohort): id, userId (unique), platformId, landedAt (nullable — set by recordLanding on chat-page mount), chattedAt (nullable — set on first message; drives the cap).
chattedAt and landedAt (WHERE NOT NULL); FKs to user + platform (CASCADE on delete).chattedAt IS NOT NULL are the rollout cohort (capped at 200). The cap COUNT is intentionally global (not tenant-scoped) — a single cloud-wide cohort.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 idlistConversations() — cursor-paginated list of user's conversations, ordered by creation date descending; excludes messages, uiMessages, and summary columns for performancegetConversationOrThrow() — fetches a conversation, enforcing ownership (platformId + userId); auto-recovers stale STREAMING conversations to IDLE after a 2-minute timeoutupdateConversation() — updates title and/or modelNamedeleteConversation() — deletes a conversation after ownership check; blocked while status is STREAMINGgetMessages() — reconstructs ChatHistoryMessage[] from stored ModelMessage[]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.ap_set_session_title — auto-names the conversation after the first exchangeap_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 completionap_list_across_projects — lists flows, tables, runs, or connections across all user-accessible projectsap_deselect_project — clears the selected project contextap_explore_data — read-only exploration of the user's data (sheets, channels, columns) to build understanding during discovery; never configures the automationap_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 neededap_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 phasesap_show_connection_required — prompts the user to connect a serviceap_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 externalIdsap_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 bubbleap_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 cardap_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 automationPOST /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 (chatRolloutService.recordLanding, deduped per user) and push the refreshed funnel snapshot to console via sendRolloutFunnelUpdate; cloud-only, 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.
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 thisgetChatConfig 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 clientrun-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)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)POST /tool-approvals/:gateId, unblocking the gate via Redis pub/subchatAnalyticsTelemetry 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 conversationschatAnalyticsTelemetry.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