Skip to main content
Apps built on Second can trigger AI agents defined in an agents.json file. Each agent has its own system prompt, scoped tools (built-in or custom HTTP), and optional write access to the app’s data. The same governed file can also define top-level appTools: custom HTTP actions callable directly from app code through the SDK for the narrow deterministic backend-function exception, such as bounded bulk fetches followed by app-side post-processing. Live runtime uses the approved configuration for that app version, so draft changes cannot quietly expand what an agent or app action can call.

How it works

App iframe (useAgent hook)
  → postMessage("second:agent:trigger")
    → AppAgentBridge (parent window)
      → POST /api/.../agent-runs              (create run)
      → POST /api/.../agent-runs/{runId}/stream?startOnly=1
        (start agent without opening a browser SSE stream)
        → Worker POST /sessions/{appId}__agent__{runId}/agent-run
          → AgentRunManager runs agent in background
          → Worker calls tools, writes data
          → Worker POSTs /api/internal/agent-run-complete when done
The app triggers an agent via the SDK. The platform creates a run record and starts the agent on the worker with a short server request. Opening the Agent Runs drawer later attaches an SSE viewer for that specific run, but normal app-agent execution does not reserve a browser connection for every running agent. The worker owns the agent lifecycle — closing the browser doesn’t stop it. App-agent routes are scoped by the full {workspaceId, appId, runId} tuple. A run from one app cannot be loaded or streamed through another app route, even inside the same workspace. App-callable integration actions use the same iframe bridge shape, but they do not create an app-agent run:
App iframe (callIntegrationTool)
  → postMessage("second:integration:execute")
    → AppIntegrationBridge (parent window)
      → POST /api/.../app-tools/{toolName}/execute
        → Verify approved agents.json appTools policy
        → Resolve app-scoped integration grant
        → Inject static secrets or current viewer OAuth token server-side
        → Execute bounded HTTP request
        → Return response to app code
Use agents for most integration-backed workflows. Use app actions only for the narrow deterministic backend-function exception: the app can fetch bounded provider batches and then page, group, filter, or aggregate the response itself without AI reasoning, such as fetching PostHog event batches and grouping them by user ID. This avoids consuming an agent’s limited context window with huge tool responses when the task is just API pagination plus local computation. Use agents when the task needs reasoning, generation, autonomous decisions, or natural-language workflows.

agents.json

Agents are defined in a JSON file in the app workspace, persisted alongside source files in MongoDB. The builder agent creates this file during the build flow and presents it via the present_agents tool for approval.
{
  "appTools": [
    {
      "type": "custom",
      "name": "posthog_events_page",
      "displayName": "Fetch PostHog events page",
      "description": "Fetches one bounded page of PostHog events for app-side grouping.",
      "enabled": true,
      "integration": {
        "name": "PostHog",
        "domain": "posthog.com",
        "keySlug": "default"
      },
      "endpoint": {
        "method": "GET",
        "url": "https://app.posthog.com/api/projects/{{projectId}}/events/",
        "headers": { "Authorization": "Bearer {{secrets.POSTHOG_PERSONAL_API_KEY}}" },
        "queryParams": { "after": "{{after}}", "before": "{{before}}", "limit": "{{limit}}" }
      },
      "mockData": [
        { "results": [{ "distinct_id": "user_123", "event": "$pageview" }], "next": null },
        { "results": [{ "distinct_id": "user_456", "event": "signup" }], "next": null },
        { "results": [], "next": null }
      ]
    }
  ],
  "agents": [
    {
      "id": "lead-enricher",
      "name": "Lead Enricher",
      "description": "Searches the web to find current information about leads",
      "systemPrompt": "You are a lead enrichment specialist...",
      "dataCollections": ["leads"],
      "tools": [
        {
          "type": "builtin",
          "name": "WebSearch",
          "enabled": true,
          "recommended": true
        },
        {
          "type": "custom",
          "name": "hubspot_fetch_contacts",
          "displayName": "Fetch Contacts",
          "description": "Search and retrieve contacts from HubSpot CRM",
          "enabled": true,
          "recommended": true,
          "integration": {
            "name": "HubSpot",
            "domain": "hubapi.com",
            "setupSearchQuery": "How to get HubSpot API key"
          },
          "endpoint": {
            "method": "GET",
            "url": "https://api.hubapi.com/crm/v3/objects/contacts",
            "headers": { "Authorization": "Bearer {{secrets.HUBSPOT_PRIVATE_APP_TOKEN}}" },
            "queryParams": { "query": "{{query}}", "limit": "10" }
          },
          "responseSchema": {
            "type": "object",
            "description": "HubSpot contacts response"
          },
          "mockData": [
            { "id": "101", "properties": { "firstname": "Sarah", "lastname": "Chen" } },
            { "id": "102", "properties": { "firstname": "Marcus", "lastname": "Johnson" } },
            { "id": "103", "properties": { "firstname": "Priya", "lastname": "Patel" } }
          ]
        }
      ]
    }
  ]
}
agents may be empty when an app only needs app actions. appTools use the same custom HTTP shape and integration rules as agents[].tools, but the caller is app code rather than an AI agent.

Key fields

FieldPurpose
idUnique identifier the SDK uses to trigger the agent
tools[].type"builtin" (WebSearch/WebFetch) or "custom" (HTTP request via tool-execute)
tools[].displayNameOptional human-readable action label for tool cards (for example "Company Lookup" while name remains clearbit_company_lookup)
tools[].enabledToggle — user can disable from the agents page
tools[].recommendedUI label — “Highly Recommended” badge. Does not enforce anything
tools[].integrationLinks to an app-scoped integration grant, matched by domain and keySlug ("default" when omitted)
tools[].endpointHTTP request spec with static named secret placeholders such as {{secrets.SLACK_BOT_TOKEN}}, normal tool-input placeholders for OAuth tools, or public no-auth requests
tools[].integration.authOptional auth metadata. Missing means either a static-secret tool when the endpoint uses {{secrets.NAME}}, or a public unauthenticated tool when the official API requires no credentials; type: "oauth2" means the broker resolves the triggering user’s connected account
tools[].mockDataSample responses used when the integration is not configured. Must contain 3+ entries for variety
dataCollectionsCollections this agent can read/write via update_app_data and read_app_data tools. See App Data
appTools[]Optional top-level custom HTTP actions callable from generated app code with callIntegrationTool
Endpoint specs can also use placeholders from the tool input, such as {{symbol}}, {{query}}, or {{company.ticker}}, in the URL, headers, query params, and body. The agent must pass a JSON string with those fields when calling the custom tool. Missing placeholders fail clearly instead of calling broad static endpoints. present_agents validates every custom tool and app action before approval. Custom tools must include integration.name, integration.domain, endpoint.method, and endpoint.url. Static tools must include a named secret placeholder such as {{secrets.SLACK_BOT_TOKEN}} where the saved integration secret is injected. OAuth tools must include integration.auth.type = "oauth2", providerKey, identity: "triggering_user", authorization URL, token URL, and exact scopes; they must not include {{oauth.access_token}}, {{access_token}}, {{token}}, {{secrets.*}}, or an explicit Authorization header. Public unauthenticated tools may omit secrets and auth metadata when the official API requires no API key, OAuth client, or token. Custom tools that need setup should include integration.keySlug and use the same slug in integration-setup.json; Second normalizes missing slugs to "default". If a model omits the endpoint or uses unsafe placeholders, the agents card is marked as needing changes and cannot be approved until the builder fixes agents.json and calls present_agents again. If an agent passes tool input to a custom tool whose endpoint does not reference any input placeholders, execution fails rather than calling a static bulk endpoint. This protects integrations from returning broad datasets when the intended request was a lookup. report_tool_call_failed is a reserved platform tool name in the app_tools namespace. Generated custom tools cannot use it. The worker exposes mcp__app_tools__report_tool_call_failed to app agents so a blocked custom-tool failure can be sent back to the builder agent for repair.

Automatic tool failure recovery

When a custom HTTP tool fails with a real execution error, the worker keeps a bounded, redacted record of the failed call for that app-agent run. The record includes the custom tool name, parsed tool input, endpoint and integration metadata from the approved agent configuration, the internal tool-execute status, and the structured error/response details. It does not include injected secret values, OAuth tokens, cookies, or full unbounded responses. If the failed tool blocks the requested app-agent task, the app agent should finish any unaffected work and then call mcp__app_tools__report_tool_call_failed. That tool posts the report to /api/internal/tool-failure-report, which verifies the app-agent run by { workspaceId, appId, runId }, creates a builder repair run for the same app, and records an audit event with compact metadata and hashes. The builder repair run starts with a platform-generated prompt that tells the builder to inspect agents.json, app code, integration setup, and the app-agent prompt, then re-present governed agents or setup instructions when they change. Workspace realtime only carries compact builder-run hints such as run id, status, and runReason: "app_tool_failure". The sidebar derives its “Call failed - builder fixing it” badge from the latest authorized builder-run projection and clears it when that run completes or fails.

Agent config approval

agents.json is draft source, but it is also runtime policy. Second treats it as a governed artifact:
  1. present_agents reads and validates the file on disk.
  2. The Agents card shows the exact parsed payload.
  3. A workspace admin or owner approves that payload.
  4. The platform stores a versioned canonical JSON hash, the normalized approved payload, the approver, and the approval time.
  5. Draft app-agent runtime can start from the draft file, but live custom tools, app-callable actions, and app-data tools are usable only while the current agents.json hash matches that approval.
The approval hash is semantic, not a raw byte-for-byte file hash. The current v1 approval schema normalizes harmless empty optional arrays before hashing: top-level appTools: [], agent-level tools: [], and agent-level dataCollections: [] are treated the same as omitted fields. Stored hashes are prefixed with the approval schema version, for example v1:<sha256>. Future agents.json schema changes should add a new approval schema version instead of changing old normalization behavior, so already-approved configs do not become stale unless their effective runtime policy changed. Creating agents.json or showing the Agents card does not send the app to review. It only pauses the builder until an admin/owner approves or someone requests changes. Review is created later from the publish dialog. If the builder, the user, a file edit, or a shell command changes agents.json after approval, the draft approval becomes stale. Draft agent runs may still start so the in-progress app can be tested, but custom HTTP tools and agent data tools are blocked until an admin or owner approves the new payload. When a review is approved or an admin/owner publishes directly, the approved payload is promoted with the published source snapshot. This prevents a draft from quietly adding a new integration domain, endpoint, secret placeholder, app action, permission, or data collection after IT has reviewed a different config.

integration-setup.json

When custom tools or app actions require an external service, the builder may also create integration-setup.json at the app workspace root. This file is separate from agents.json. agents.json defines what the agent or app code can call; integration-setup.json explains what a human needs to configure in the provider. The builder creates this file only when setup is needed:
  • this app has no connected integration grant for that domain/key slug
  • the app grant exists, but this app requires new permission groups, exact permissions/scopes, or named secrets that are not marked configured
{
  "integrations": [
    {
      "name": "Slack",
      "domain": "slack.com",
      "keySlug": "default",
      "keyName": "Slack post key for this app",
      "capabilityLabel": "Slack post",
      "why": "This app sends Slack messages.",
      "permissionGroups": [
        {
          "name": "Send messages",
          "description": "Allows the app to post messages into selected Slack channels.",
          "permissions": ["chat:write"]
        }
      ],
      "secrets": [
        {
          "name": "SLACK_BOT_TOKEN",
          "label": "Slack bot token",
          "description": "Paste the Bot User OAuth Token that starts with xoxb-.",
          "required": true
        }
      ],
      "setupInstructions": {
        "overview": "Create or update a Slack app, grant the bot scope, install it to the workspace, and paste the bot token in Second.",
        "steps": [
          {
            "title": "Open Slack apps",
            "description": "Go to [Slack | API apps](https://api.slack.com/apps) and create a new app or open the existing app you want Second to use.",
            "url": "https://api.slack.com/apps"
          }
        ],
        "links": [
          { "label": "Slack API apps", "url": "https://api.slack.com/apps" }
        ]
      }
    }
  ]
}
Before writing the file, the builder calls list_app_integration_keys to check the current app’s grant state without receiving secret values. Another app’s credential does not satisfy this app. After the runtime policy is approved, the builder writes integration-setup.json and calls present_integration_setup before app implementation continues. The chat UI shows a compact “Instructions on how to set up …” card, and the worker syncs the setup metadata into the integrations settings page immediately. If requirements change later, the builder updates integration-setup.json with the complete current requirements and calls present_integration_setup again, which replaces this app’s grant set and re-syncs the integrations page. If the file is missing or invalid JSON, the platform does not register the integration. The file should use simple human language and verified links, not developer-only notes.

Security policy

An agent with access to organization tools (HubSpot, Slack, etc.) must not also have internet access (WebSearch/WebFetch). These should be separate agents in a multi-agent setup. The builder agent’s system prompt enforces this separation.

App agent SDK

The SDK is included in the workspace template at src/lib/second-sdk.ts. It communicates with the platform via postMessage.

useAgent(agentId)

Triggers an agent and watches its status live.
import { useAgent } from '@/lib/second-sdk';

function EnrichButton({ leadId }: { leadId: string }) {
  const { trigger, status, isRunning } = useAgent('lead-enricher');

  return (
    <button
      onClick={() => trigger(`Enrich lead ${leadId}`)}
      disabled={isRunning}
    >
      {isRunning ? 'Enriching...' : 'Enrich'}
    </button>
  );
}
Return valueTypeDescription
trigger(prompt)(prompt: string) => voidStart the agent with a user prompt
status'idle' | 'running' | 'completed' | 'failed'Current run status
isRunningbooleantrue while the agent is executing
errorstring | nullError message if the run failed
runIdstring | nullCurrent run ID

useAgentList()

Returns all agents defined in agents.json.
const { agents } = useAgentList();
// agents: Array<{ id: string; name: string; description: string }>

callIntegrationTool(toolName, input)

Calls a top-level appTools[] action from app code and returns the provider response to the iframe without exposing secrets or OAuth tokens.
import { callIntegrationTool } from '@/lib/second-sdk';

type EventsPage = {
  results: Array<{ distinct_id?: string; event?: string }>;
  next?: string | null;
};

const result = await callIntegrationTool<
  { projectId: string; after?: string; before?: string; limit: number },
  EventsPage
>('posthog_events_page', {
  projectId: '123',
  after: '2026-05-01',
  before: '2026-05-20',
  limit: 100,
});

if (!result.success) {
  throw new Error(result.error ?? 'PostHog request failed');
}
The route resolves the approved app action server-side from agents.json; the iframe sends only toolName and input. Static secrets are read from the app-scoped integration grant. OAuth app actions use the current app viewer as the triggering_user identity. After a successful callIntegrationTool call, if the processed result should survive refresh, be shared across users, or be reused by the app/agents later, save the compact processed result with useCollection/useDoc insert or update. Do not persist huge raw provider responses unless the app truly needs them. Make sure to structure the request and post-process the response in code so it can be beautifully saved in the app, not as huge raw chunks. Live failures return actionable diagnostics, not only a boolean failure. The result can include error, statusCode, errorCode, errorCategory, resolution, retryable, canRequestBuilderRepair, and bounded redacted details. Generated apps should render error plus resolution directly. Credential and permission failures should point the user back to integration setup; repairable endpoint/input/spec failures can offer an “Ask builder to fix” action that calls reportIntegrationToolFailure.

reportIntegrationToolFailure(toolName, input, result, description)

Reports a blocking, repairable app-callable backend function failure from the iframe to the normal builder run. The browser route re-authenticates the current viewer, requires editable draft access, resolves the approved tool spec server-side, redacts and bounds the failure payload, then schedules a builder recovery run. Do not use this for wrong or expired API keys, missing scopes, or provider access problems that only the integration owner can fix. Use it when app code, typed wrappers, agents.json, or setup instructions likely need repair.

PostMessage protocol

// App → Platform
second:agent:trigger      { agentId, prompt }
second:agents:list-request  {}
second:integration:execute  { toolName, input }
second:integration:report-failure { toolName, input, result, description, attemptedTask? }

// Platform → App
second:agent:update       { agentId, runId, status, result?, error? }
second:agents:list-response { agents: [...] }
second:integration:execute-response { success, data?, mock, mockReason?, statusCode?, error?, errorCode?, errorCategory?, resolution?, retryable?, canRequestBuilderRepair?, details? }
second:integration:report-failure-response { ok, status?, builderRunId?, error? }

present_agents tool

The builder agent calls present_agents after writing agents.json. This renders an interactive card in the chat showing each agent and each top-level app action with its tools, integration requirements, and recommended labels. It is an approval stop: the tool returns the card payload, the runtime adapter stops the active turn, and the chat composer stays blocked until an admin/owner approves or someone requests changes from the agents card. The tool is registered as mcp__second__present_agents in the worker’s second MCP server alongside present_plan, list_app_integration_keys, present_integration_setup, and done_building.
const presentAgents = tool(
  "present_agents",
  "Present the agent configuration to the user for approval...",
  {},
  async () => {
    const agentsConfig = JSON.parse(readFileSync("agents.json", "utf-8"));
    // Validate agentsConfig.agents and return the card payload.
  },
);
present_agents validates the agents.json file on disk as the source of truth, including custom integration tool and app action shape, and returns a fix-it message if the file is missing, invalid JSON, empty, or uses invalid custom-tool placeholders. A file with no agents is valid when it has a non-empty top-level appTools array. When an admin or owner approves, AppChat first records the governed approval for the normalized Agents card payload, then sends the follow-up user message that continues the build. On requested changes, it sends the feedback as the next user message and the builder must update agents.json and call present_agents again.

Agent run lifecycle

Database

App agent runs are stored in the app_agent_runs collection, separate from builder agent runs (agent_runs).
type AppAgentRunDocument = {
  _id: string;
  appId: string;
  workspaceId: string;
  triggeredByUserId: string;
  triggeredByUserEmail: string;
  triggeredByUserName: string;
  sourceVersion?: "draft" | "published";
  agentId: string;
  agentName: string;
  prompt: string;
  status: "pending" | "running" | "streaming" | "completed" | "failed";
  result: unknown | null;
  messages: unknown[];
  sessionId: string | null;
  activeStreamId: string | null;
  usage: RunUsage | null;
  createdAt: Date;
  updatedAt: Date;
};

Flow

  1. App triggers agent — SDK sends second:agent:trigger via postMessage.
  2. Bridge creates runAppAgentBridge calls POST /api/.../agent-runs → creates AppAgentRunDocument with status: "pending" and the server-resolved triggering user.
  3. Bridge starts run — Calls POST /api/.../agent-runs/{runId}/stream?startOnly=1.
  4. Stream route starts worker — Calls POST {WORKER_URL}/sessions/{appId}__agent__{runId}/agent-run (fire-and-forget). Worker returns { status: "started" } immediately.
  5. Worker runs agentAgentRunManager spawns the agent in the background. Buffers messages via EventEmitter. When a custom tool runs, the worker sends runId to /api/internal/tool-execute; it does not send a user ID for OAuth.
  6. Optional drawer viewer reads events — If someone opens the run details drawer, the stream route connects to GET {WORKER_URL}/sessions/{appId}__agent__{runId}/agent-run/{runId}/events and translates SDK events to UIMessageStream via the bridge, same as builder agent streaming.
  7. Agent finishes — Worker calls POST /api/internal/agent-run-complete with status, result, usage, and the SDK message transcript. The web route converts that transcript to UI messages and updates the run document.
  8. Bridge receives completion — The bridge listens for compact workspace run events and posts second:agent:update to the iframe. A low-frequency summary poll is kept only as a missed-event fallback.

Streaming in the agents drawer

App agent execution and viewing are separate. Triggering an app agent starts it with startOnly=1, which returns quickly and leaves the worker-owned run active in AgentRunManager. The browser opens an app-agent SSE stream only when the user explicitly opens a run in the Agent Runs drawer, so several app agents can run without consuming several long-lived browser connections. App agent viewer streams use the same UI message format as the builder agent. When a viewer stream exists, the route records UI chunks in the Redis replay buffer used by builder runs. Reopening the agents drawer first tries the active resumable stream; if that handle is stale, it can replay a complete captured buffer, or rebuild the transcript from the worker events endpoint while the worker still has the run in memory. Replay buffers are only used when they start at the beginning of the run, so the UI is not asked to process a tool-input-delta without the matching tool-input-start. Closing the run viewer aborts the browser fetch and the web route forwards that disconnect signal to the worker events fetch. This releases the viewer connection without stopping the background app-agent run, and clears the transient active stream id if that viewer owned it. The drawer and iframe status bridge use projected run summaries for list and status polling. Full UIMessage[] transcripts stay behind the explicit run viewer path, keeping navigation and the Agent Runs dropdown off the hot transcript path.

Async execution (AgentRunManager)

The worker runs app agents via AgentRunManager (apps/worker/src/agent-run-manager.ts), an in-memory event-driven system that decouples agent execution from the SSE viewer. Each app-agent run is keyed by runId, and the web server uses a per-run worker session key so multiple app agents can run at the same time without sharing one SDK transport.

Design

  • Fire-and-forget: start(config) spawns the agent in the background and returns immediately.
  • Event buffering: All SDK messages are buffered in memory. Late-joining SSE viewers catch up on buffered messages, then receive live events.
  • EventEmitter: Each run has its own EventEmitter. Viewers subscribe via events(runId) — an async generator that yields buffered messages first, then live messages.
  • Callback: When the agent finishes, the manager POSTs to callbackUrl (/api/internal/agent-run-complete) with the final status, result, and usage.
  • Cleanup: Completed runs are kept for 30 minutes (for late-joining viewers), then evicted. Max 100 concurrent runs with LRU eviction of completed/failed runs.

Why this exists

The original plan described a simple fire-and-forget with a callback. In practice, the SSE viewer needs to both catch up on past messages AND receive live events from a background agent. The AgentRunManager with its event buffering and EventEmitter solves this — it’s the bridge between the background agent and any number of SSE viewers.

Worker endpoints

POST /sessions/:appId/agent-run

Start a background agent run. Returns immediately. Request body:
{
  "runId": "abc-123",
  "prompt": "Enrich lead Sarah Chen",
  "systemPrompt": "You are a lead enrichment specialist...",
  "agentConfig": { "id": "lead-enricher", "tools": [...], "dataCollections": ["leads"] },
  "allowedTools": ["WebSearch", "WebFetch", "mcp__app_data__update_app_data"],
  "workspaceId": "ws-1",
  "appId": "app-1",
  "callbackUrl": "http://web:3000/api/internal/agent-run-complete",
  "sourceFiles": { "src/App.tsx": "..." }
}
Response: { "status": "started", "runId": "abc-123" }

GET /sessions/:appId/agent-run/:runId/events

SSE stream of raw SDK messages from a running (or recently completed) agent. Yields buffered messages first, then live events.

API routes

App agent runs

MethodPathPurpose
POST/api/workspaces/[wId]/apps/[aId]/agent-runsCreate a new run (status: pending)
GET/api/workspaces/[wId]/apps/[aId]/agent-runs/[rId]Get run status and result
POST/api/workspaces/[wId]/apps/[aId]/agent-runs/[rId]/streamStart agent + return SSE stream
GET/api/workspaces/[wId]/apps/[aId]/agent-runs/[rId]/streamResume disconnected stream

Agents config

MethodPathPurpose
GET/api/workspaces/[wId]/apps/[aId]/agentsGet draft agents.json for app creators/collaborators or published agents.json for viewers
PATCH/api/workspaces/[wId]/apps/[aId]/agentsAdmin/owner/app creator: update draft agents.json in the draft source snapshot
POST/api/workspaces/[wId]/apps/[aId]/agents/approvalAdmin/owner: approve the exact draft agents.json payload

App integration actions

MethodPathPurpose
POST/api/workspaces/[wId]/apps/[aId]/app-tools/[toolName]/executeBrowser-authenticated app action execution through the parent bridge
POST/api/workspaces/[wId]/apps/[aId]/app-tools/[toolName]/report-failureBrowser-authenticated draft-only builder repair report for app-callable backend functions

Internal (worker → web)

MethodPathPurpose
POST/api/internal/agent-run-completeWorker callback when agent finishes
POST/api/internal/tool-executeExecute custom HTTP tool (see Integrations)
POST/api/internal/tool-failure-reportCreate a builder repair run from a blocked app-agent custom-tool failure
POST/api/internal/integration-requirementsSync builder-requested integration setup metadata
POST/api/internal/workspace-integrationsReturn live integration metadata to the builder without secret values
Internal endpoints bypass the browser auth proxy and authenticate via INTERNAL_API_TOKEN. Local development can omit the token; production requires it on both web and worker. See Guard and Tenancy — Internal API bypass.

Custom tool execution

Custom tools and app actions (type "custom" in agents.json) are HTTP requests to external APIs. The agent and iframe never see API secrets, OAuth client secrets, refresh tokens, or access tokens. Agent tool execution is proxied through the web server’s /api/internal/tool-execute endpoint, which verifies that the requested tool is present in the approved agents.json payload for the calling agent. App action execution uses /api/workspaces/[wId]/apps/[aId]/app-tools/[toolName]/execute, which resolves the canonical approved top-level appTools[] item server-side. For static tools, the shared executor injects named secrets and non-secret tool input placeholders at call time. For OAuth agent tools, it loads the app-agent run by { workspaceId, appId, runId }, resolves triggeredByUserId from that server-created row, checks the user’s connected account and scopes, refreshes the access token on demand if needed, injects Authorization: Bearer <token> server-side, and calls the provider API. For OAuth app actions, it uses the current authenticated app viewer as the OAuth user. See Integrations for the full flow. When an integration is not configured, the tool returns a random entry from mockData so development can continue without real API credentials. This includes OAuth missing-account, revoked-account, missing-scope, or provider-config failures. When a custom tool fails after execution begins, the app agent may call mcp__app_tools__report_tool_call_failed. Generated app code may call reportIntegrationToolFailure for repairable app-callable backend function failures while testing the editable draft. Both report paths create a normal builder run, so repair uses the same chat streaming, source persistence, present_agents, present_integration_setup, and done_building controls as any other build change.

Key files

FileRole
apps/worker/src/runner.tslist_app_integration_keys, present_agents, present_integration_setup, buildCustomToolsMcpServer, buildAppDataMcpServer
apps/worker/src/agent-run-manager.tsBackground agent execution with event buffering
apps/worker/src/index.ts/sessions/:appId/agent-run and events endpoints
apps/web/src/components/app-agent-bridge.tsxpostMessage bridge — triggers agents, listens for status events
apps/web/src/components/app-integration-bridge.tsxpostMessage bridge — executes app-callable integration actions
apps/web/src/app/api/.../agent-runs/route.tsCreate run
apps/web/src/app/api/.../agent-runs/[rId]/stream/route.tsStart agent + SSE stream
apps/web/src/app/api/internal/agent-run-complete/route.tsWorker completion callback
apps/web/src/app/api/internal/tool-execute/route.tsInternal app-agent custom tool approval enforcement
apps/web/src/app/api/.../app-tools/[toolName]/execute/route.tsBrowser-authenticated app integration action execution
apps/web/src/app/api/.../app-tools/[toolName]/report-failure/route.tsDraft-only builder recovery reports from generated app code
apps/web/src/lib/integrations/execute-http-action.tsShared HTTP action executor with secret/OAuth injection, domain/IP guards, size limits, and mock fallback
apps/web/src/app/api/internal/tool-failure-report/route.tsApp-agent tool failure recovery bridge to the builder
apps/web/src/app/api/internal/integration-requirements/route.tsIntegration requirement sync from builder tools
apps/worker/src/workspace-template.tsSDK with useAgent, useAgentList, callIntegrationTool, and useIntegrationTool