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
{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:
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 thepresent_agents tool for approval.
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
| Field | Purpose |
|---|---|
id | Unique identifier the SDK uses to trigger the agent |
tools[].type | "builtin" (WebSearch/WebFetch) or "custom" (HTTP request via tool-execute) |
tools[].displayName | Optional human-readable action label for tool cards (for example "Company Lookup" while name remains clearbit_company_lookup) |
tools[].enabled | Toggle — user can disable from the agents page |
tools[].recommended | UI label — “Highly Recommended” badge. Does not enforce anything |
tools[].integration | Links to an app-scoped integration grant, matched by domain and keySlug ("default" when omitted) |
tools[].endpoint | HTTP 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.auth | Optional 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[].mockData | Sample responses used when the integration is not configured. Must contain 3+ entries for variety |
dataCollections | Collections 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 |
{{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 internaltool-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:
present_agentsreads and validates the file on disk.- The Agents card shows the exact parsed payload.
- A workspace admin or owner approves that payload.
- The platform stores a versioned canonical JSON hash, the normalized approved payload, the approver, and the approval time.
- 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.jsonhash matches that approval.
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 createintegration-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
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 atsrc/lib/second-sdk.ts. It communicates with the platform via postMessage.
useAgent(agentId)
Triggers an agent and watches its status live.
| Return value | Type | Description |
|---|---|---|
trigger(prompt) | (prompt: string) => void | Start the agent with a user prompt |
status | 'idle' | 'running' | 'completed' | 'failed' | Current run status |
isRunning | boolean | true while the agent is executing |
error | string | null | Error message if the run failed |
runId | string | null | Current run ID |
useAgentList()
Returns all agents defined in agents.json.
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.
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
present_agents tool
The builder agent callspresent_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.
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 theapp_agent_runs collection, separate from builder agent runs (agent_runs).
Flow
- App triggers agent — SDK sends
second:agent:triggervia postMessage. - Bridge creates run —
AppAgentBridgecallsPOST /api/.../agent-runs→ createsAppAgentRunDocumentwithstatus: "pending"and the server-resolved triggering user. - Bridge starts run — Calls
POST /api/.../agent-runs/{runId}/stream?startOnly=1. - Stream route starts worker — Calls
POST {WORKER_URL}/sessions/{appId}__agent__{runId}/agent-run(fire-and-forget). Worker returns{ status: "started" }immediately. - Worker runs agent —
AgentRunManagerspawns the agent in the background. Buffers messages viaEventEmitter. When a custom tool runs, the worker sendsrunIdto/api/internal/tool-execute; it does not send a user ID for OAuth. - 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}/eventsand translates SDK events to UIMessageStream via the bridge, same as builder agent streaming. - Agent finishes — Worker calls
POST /api/internal/agent-run-completewith status, result, usage, and the SDK message transcript. The web route converts that transcript to UI messages and updates the run document. - Bridge receives completion — The bridge listens for compact workspace run events and posts
second:agent:updateto 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 withstartOnly=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 viaAgentRunManager (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 viaevents(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. TheAgentRunManager 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:
{ "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
| Method | Path | Purpose |
|---|---|---|
POST | /api/workspaces/[wId]/apps/[aId]/agent-runs | Create 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]/stream | Start agent + return SSE stream |
GET | /api/workspaces/[wId]/apps/[aId]/agent-runs/[rId]/stream | Resume disconnected stream |
Agents config
| Method | Path | Purpose |
|---|---|---|
GET | /api/workspaces/[wId]/apps/[aId]/agents | Get draft agents.json for app creators/collaborators or published agents.json for viewers |
PATCH | /api/workspaces/[wId]/apps/[aId]/agents | Admin/owner/app creator: update draft agents.json in the draft source snapshot |
POST | /api/workspaces/[wId]/apps/[aId]/agents/approval | Admin/owner: approve the exact draft agents.json payload |
App integration actions
| Method | Path | Purpose |
|---|---|---|
POST | /api/workspaces/[wId]/apps/[aId]/app-tools/[toolName]/execute | Browser-authenticated app action execution through the parent bridge |
POST | /api/workspaces/[wId]/apps/[aId]/app-tools/[toolName]/report-failure | Browser-authenticated draft-only builder repair report for app-callable backend functions |
Internal (worker → web)
| Method | Path | Purpose |
|---|---|---|
POST | /api/internal/agent-run-complete | Worker callback when agent finishes |
POST | /api/internal/tool-execute | Execute custom HTTP tool (see Integrations) |
POST | /api/internal/tool-failure-report | Create a builder repair run from a blocked app-agent custom-tool failure |
POST | /api/internal/integration-requirements | Sync builder-requested integration setup metadata |
POST | /api/internal/workspace-integrations | Return live integration metadata to the builder without secret values |
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
| File | Role |
|---|---|
apps/worker/src/runner.ts | list_app_integration_keys, present_agents, present_integration_setup, buildCustomToolsMcpServer, buildAppDataMcpServer |
apps/worker/src/agent-run-manager.ts | Background agent execution with event buffering |
apps/worker/src/index.ts | /sessions/:appId/agent-run and events endpoints |
apps/web/src/components/app-agent-bridge.tsx | postMessage bridge — triggers agents, listens for status events |
apps/web/src/components/app-integration-bridge.tsx | postMessage bridge — executes app-callable integration actions |
apps/web/src/app/api/.../agent-runs/route.ts | Create run |
apps/web/src/app/api/.../agent-runs/[rId]/stream/route.ts | Start agent + SSE stream |
apps/web/src/app/api/internal/agent-run-complete/route.ts | Worker completion callback |
apps/web/src/app/api/internal/tool-execute/route.ts | Internal app-agent custom tool approval enforcement |
apps/web/src/app/api/.../app-tools/[toolName]/execute/route.ts | Browser-authenticated app integration action execution |
apps/web/src/app/api/.../app-tools/[toolName]/report-failure/route.ts | Draft-only builder recovery reports from generated app code |
apps/web/src/lib/integrations/execute-http-action.ts | Shared 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.ts | App-agent tool failure recovery bridge to the builder |
apps/web/src/app/api/internal/integration-requirements/route.ts | Integration requirement sync from builder tools |
apps/worker/src/workspace-template.ts | SDK with useAgent, useAgentList, callIntegrationTool, and useIntegrationTool |