Creating Custom Apps
Custom apps extend what your agents can do by running your code in an isolated sandbox, deployed once per installation. There are two ways to build one:
- The in-dashboard IDE — for a small app with one tool and a few files. Everything happens inside the Sprigr dashboard: a Monaco editor with a file tree, a schema tab, a test console, and a Deploy button.
- The app kit — for full marketplace apps: many tools, OAuth to an external system, webhooks, schedules, durable jobs, settings pages, and per-install database migrations. These are authored in your own repository with the
@sprigr/apps-app-sdkpackage and published from the terminal with thesprigr appCLI.
Both paths produce the same thing: a per-install Worker that exports tool handlers the platform dispatches to. This guide covers the contract those handlers follow, then each build path.
The handler contract
Section titled “The handler contract”An app’s code exports a map from tool name to handler. Each handler receives the call arguments, the install’s environment bindings, and a context object:
// The default export is { [toolName]: (args, env, ctx) => Promise<unknown> }export default { weather_check: async (args, env, ctx) => { const response = await fetch( `https://api.weatherapi.com/v1/forecast.json?key=${env.WEATHER_API_KEY}&q=${args.postcode}&days=3` ); if (!response.ok) throw new Error(`Weather API returned ${response.status}`); const data = await response.json(); return { location: data.location.name, forecast: data.forecast.forecastday.map(day => ({ date: day.date, condition: day.day.condition.text, maxTemp: day.day.maxtemp_c, chanceOfRain: day.day.daily_chance_of_rain })) }; }};args— For a tool call, the parsed JSON input the agent supplied (validated against the tool’s input schema first). For a webhook handler it is{ body, signature, headers }; for an event handler, the event payload; for a schedule handler,{ schedule, cron, timezone, scheduled_at, fired_at }.env— The install’s bindings:DB(a per-install D1 database allocated on first build),KV(a per-install KV namespace, only emitted if used),INSTALL_ID,COMPANY_ID,APP_SLUG,SPRIGR_INSTALL_TOKEN,SPRIGR_PLATFORM_BASE, and one binding per secret the manifest declares. Tool, event, and webhook handlers also getenv.SPRIGR, described below.ctx— Invocation context for the call.
The return value is serialised to JSON and passed back to the agent. Keep your output structured and concise — agents work best with clean, well-labelled data rather than raw API dumps.
Secrets and credentials
Section titled “Secrets and credentials”Secrets are declared in the manifest (secrets[]) and arrive on env under their own names, so env.WEATHER_API_KEY is the value the installing company entered. They are supplied per installation at install time, stored encrypted, and injected at runtime — they never appear in your code, in agent conversations, or in a response to an API call. A secret can instead be marked publisher_provides (you seed it once for every install with sprigr app set-publisher-secrets) or auto_generate (the platform mints a random value per install, useful for webhook signing keys).
Installers can rotate a secret at any time from the app’s Configure page; the new value applies on the next invocation with no redeploy.
Outbound HTTP
Section titled “Outbound HTTP”Use the standard fetch() API. Your manifest declares the external domains the app talks to under permissions.network_domains. That list is declarative, not enforced: it validates templated OAuth hosts, lets the agent route a blocked outbound request to your app instead of prompting for raw access, and tells reviewers what the app contacts, but the runtime does not block requests to hosts you left out. Declare every host you call (OAuth login hosts included) so those paths and reviewers are correct, and do not treat it as a sandbox boundary.
env.SPRIGR: calling back into the platform
Section titled “env.SPRIGR: calling back into the platform”Tool, event, and webhook handlers get an injected env.SPRIGR object. Every method calls the platform over the per-install token, so your app never holds a Sprigr API key and every call is scoped to the install’s own company. The main surface:
| Call | What it does |
|---|---|
emit(name, payload) | Emit a marketplace event to subscribers (and cross-tenant fan-out) |
invoke(tool, args) | Call a tool from another app your app depends on |
integrations.invoke(req) | Call one of the company’s connected built-in integrations |
collections.* | Define, ingest, query, reconcile, describe, and read history on typed Collections |
data.{import,search,get,delete,listIds} | A private per-company discovery index for caching slow upstream data (declare data_index in the manifest) |
schedules.create(args) | Self-provision an agent-side scheduled task |
run_workflow(id, opts) | Run a tenant workflow synchronously (decision points) |
jobs.{start,get,signal,cancel,list} | Durable, resumable multi-step jobs (declare jobs[]; needs the sprigr.jobs scope) |
store.{get,put,delete,list} | Company- or publisher-scoped key-value store (needs sprigr.jobs; publisher scope needs sprigr.jobs:publisher) |
files.{putStream,url} | Durable app-scoped file storage with signed download URLs |
browser.fetch / browser.screenshot | One-shot headless fetch or screenshot (needs sprigr.browser:fetch) |
browser.session.* | Stateful, cookie-persistent browser sessions, available only to the app’s publisher running its own app (needs sprigr.browser:session) |
inbox.append, usage.report | Mirror mail into the Sprigr inbox; meter usage |
Scopes are requested under permissions.scopes in the manifest and shown on the install consent screen.
Calling other apps and integrations
Section titled “Calling other apps and integrations”An app can depend on specific tools from another marketplace app (app_dependencies[]) and call them with env.SPRIGR.invoke, or on specific tools of the company’s connected integrations (integration_dependencies[]) and call them with env.SPRIGR.integrations.invoke. Each declared tool is surfaced on the install consent screen and becomes a grant the installing organisation can pause, resume, or revoke — see App Permissions and Grants. Mark a dependency required to refuse installation when it is missing, so the app can never end up installed but broken; leave it optional and handle both cases in code.
Response envelopes
Section titled “Response envelopes”A handler’s return value can carry reserved top-level keys that the platform acts on before the agent sees the result:
_approval— Pause for a real human sign-off before the action runs. Wrap sensitive tools withrequireApprovalfrom@sprigr/apps-app-sdk, describing the question, the record affected, and optionally how to undo it. The person sees an approval card in chat naming the specific record._undo— Record a before-image so the action can be reversed later. Declare the reverse tool in the manifest (undo.reverse_tool, markedinternal: true) and implement it withrunUndoApplyand the@sprigr/apps-undo-journalpackage._artifacts— Render an interactive app card in chat:{ type: 'panel', panelKind: 'app_card', title, panelData }with a stablecard_id, optional images, labelled fields, a table, and up to six action buttons that invoke your tools directly when clicked. Cards are stripped from what the agent sees, so the JSON response must still be a complete answer on its own. Markup is never rendered; any tag-like string drops the card.
For actions that only need the agent to double-check rather than a human tap, set a per-tool confirmation policy in the manifest (tools[].confirmation, with always, when.count, or per-action rules).
Building in the dashboard IDE
Section titled “Building in the dashboard IDE”-
Navigate to the Apps page
From the dashboard sidebar, click Apps. This shows all the apps your company has created or installed, along with their versions.
-
Click Create App
Click the Create App button and fill in the creation form:
- Name — The app’s display name. The URL slug is derived from it.
- Description — A plain-language explanation of what the app does.
- Category — One of Data, Productivity, Tools, Communication, CRM, or Other.
- Tool Name — The identifier agents see when deciding which tool to use, like
weather_checkorinvoice_lookup. Must be lowercase with underscores. Your code’s default export must contain a handler under this name. - Tool Description — When an agent should use the tool. For example: “Fetches the 3-day weather forecast for a given Australian postcode. Use this when scheduling outdoor jobs to check for rain or extreme heat.”
- Code — Your starter handler code (you can refine it in the IDE afterwards).
- Input Schema — Optional JSON Schema for the tool’s inputs.
- Tags and Network Domains — Optional comma-separated lists for discovery and the declared network domains.
Submitting creates the app at version 0.1.0.
-
Open the App IDE
Click Edit on your new app to open the IDE:
- File tree (left sidebar) — Manage multiple files in your app. Create, rename, and delete files to organise your code into logical modules.
index.tsis the entry point. - Code editor (centre) — A Monaco-based editor with syntax highlighting and error markers.
- Tabs panel (right) — Three tabs: Schema (define inputs), Test (run your app), and Settings (tool name, tool description, network domains).
- Test output (bottom of the Test tab) — Shows the run status, logs, and the returned result when you run tests.
- File tree (left sidebar) — Manage multiple files in your app. Create, rename, and delete files to organise your code into logical modules.
-
Write your handler code
Follow the handler contract above. Split larger apps across files:
index.tsexports the tool map, and other files export the helpers it imports. -
Define the input schema
Switch to the Schema tab and define what inputs your tool expects. See the input schema section below.
-
Test your app
Switch to the Test tab, provide sample input JSON and any test secrets, then click Run Test. The test executes your code in a sandbox without deploying — check the output panel for logs and verify the result.
-
Deploy
When your app is working correctly, click Deploy. This bundles all your files with esbuild, increments the version number (0.1.0, 0.1.1, and so on), and makes the new version available to agents.
Multi-file projects
Section titled “Multi-file projects”For anything beyond a simple utility, split your code across files. The IDE supports a file tree where you can add, rename, and delete modules:
my-app/ index.ts ← Exports the tool map (entry point) api-client.ts ← Reusable API wrapper formatters.ts ← Data formatting helpers types.ts ← Shared type definitionsexport async function fetchWeather(postcode, apiKey) { const response = await fetch( `https://api.weatherapi.com/v1/forecast.json?key=${apiKey}&q=${postcode}&days=3` ); if (!response.ok) throw new Error(`Weather API returned ${response.status}`); return response.json();}
// index.tsimport { fetchWeather } from './api-client';
export default { weather_check: async (args, env) => { const data = await fetchWeather(args.postcode, env.WEATHER_API_KEY); return { location: data.location.name, forecast: data.forecast.forecastday.map(day => ({ date: day.date, condition: day.day.condition.text, maxTemp: day.day.maxtemp_c, chanceOfRain: day.day.daily_chance_of_rain })) }; }};When you deploy, all files are bundled into a single JavaScript file using esbuild — the platform handles the bundling automatically.
Defining the input schema
Section titled “Defining the input schema”The Schema tab uses JSON Schema to define what inputs your tool accepts. This schema serves two purposes: it validates input before your handler runs, and it tells agents what parameters they need to provide.
{ "type": "object", "properties": { "postcode": { "type": "string", "description": "Australian postcode to check weather for" }, "days": { "type": "number", "description": "Number of forecast days (1-3)", "minimum": 1, "maximum": 3, "default": 3 } }, "required": ["postcode"]}Testing before deploying
Section titled “Testing before deploying”The Test tab lets you run your app in a sandboxed environment before deploying it to production, so you can iterate without affecting live agents.
- Input JSON — Paste or type the input your app should receive. It must match the schema you defined.
- Test secrets — Enter temporary secret values for testing. These are not stored and are only used for the current test run.
- Run Test — Click Run Test to execute your handler. The output panel shows the run status, any
console.log()output, and the return value.
Deploying your app
Section titled “Deploying your app”When you click Deploy, three things happen:
- Bundling — All your source files are bundled into a single JavaScript file using esbuild. Imports are resolved, TypeScript is transpiled, and the bundle is optimised.
- Version increment — The app’s version number is automatically incremented (0.1.0, 0.1.1, and so on).
- Installation sync — Installations that track the latest version are upgraded to the new version. Installations pinned to a specific version stay where they are.
Each version is immutable once deployed. If you need to roll back, installers can switch to any previous version from their installation settings.
Agents can create apps too
Section titled “Agents can create apps too”Your AI agents can create and update custom tools on your behalf using the manage_team tool with the create_custom_tool action (alongside list_custom_tools, update_custom_tool, test_custom_tool, and delete_custom_tool). Describe what you want the tool to do, and the agent writes the code, defines the schema, and deploys it — all within the conversation.
This is particularly useful for one-off utilities or quick integrations where writing code in the IDE feels like overkill. Agent-created apps appear on the same Apps page and can be edited in the IDE like any other app.
Building a full app with the app kit
Section titled “Building a full app with the app kit”Full marketplace apps are Next.js projects that run isolated per install, keep per-install state in their own D1 database, and connect to third-party systems via OAuth through Sprigr’s shared bouncer. They are built with the app kit, the sprigr-app-kit repository, which contains:
@sprigr/apps-app-sdk— the SDK: OAuth state codec, Web Crypto helpers, retryingfetch, app-scoped file storage,requireApproval,runUndoApply, and the types forenv.SPRIGR.- Companion packages —
@sprigr/apps-oauth-utils(code exchange and race-safe token refresh),@sprigr/apps-d1-kv(token and settings stores),@sprigr/apps-sync-cursor,@sprigr/apps-dedup-latch,@sprigr/apps-undo-journal,@sprigr/apps-webhook-registry,@sprigr/apps-faceted-search,@sprigr/apps-dashboard-kit, and@sprigr/apps-timezone-picker. - Reference apps — a complete OAuth app (
harvest) and a synthetic every-feature app (showcase, withshowcase-consumerandstatic-badge) indexed by a capability cookbook that maps each manifest field to a working sample. - Guides — getting started, a step-by-step build guide, the platform reference (manifest schema, runtime bindings, publish pipeline, bouncer contract), the capability cookbook, and a write-protection guide for approval and undo.
The workflow is: scaffold with pnpm create:app <name>, fill in the manifest and handlers, exercise handlers and the OAuth callback locally with sprigr app dev, check the bundle with sprigr app validate, then sprigr app publish. See Deploy CLI for the full sprigr app command list.
The manifest
Section titled “The manifest”A full app is described by a sprigr-app.json manifest. Beyond tools[] (each with a handler file, input_schema, optional output_schema, and optional confirmation policy), it can declare:
secrets[]— install-time, publisher-provided, or auto-generated secretspermissions—network_domainsandscopeswebhooks[]— inbound webhook receivers with HMAC, bearer, OIDC, or handshake verification, optionally shared across installs with per-tenant fan-outschedules[]andagent_schedules[]— cron handlers that run in the app, and scheduled tasks provisioned on the tenant’s agentsjobs[]— durable, resumable multi-step background jobs with retriesevents— events the app emits and events it subscribes to (with filters), including cross-tenant emitsdecision_points[]— points where the app hands a decision to a tenant workflow viarun_workflowdata_index— a private per-company discovery index the app can import into and searchmigrations[]— an immutable chain of SQL migrations for the per-install databasechannels[]— conversational channels the app provides (receive, send, identity)workflow_templates[]— workflows installed into the tenant’s workspace alongside the appcross_tenant_tools[],app_dependencies[],integration_dependencies[]— the grant-backed dependency surface described aboveundo,tool_access,lifecycle(on_connect/on_disconnecthooks),config_schema(per-install settings installers can override), anddocs[](AI-facing docs seeded into the platform so agents know how to use the app)
Next steps
Section titled “Next steps”Once your app is deployed, you or anyone in your organisation can install it on agents. See Installing Apps to learn about browsing, installing, and managing apps.
To make your app available to other companies, open its detail page: Share generates a private invite link, and List publicly puts it in the marketplace catalog. See the Marketplace Overview for details on trust tiers.
You can also create and manage apps via MCP from your IDE — see MCP Overview for the create_app, install_app, and publish_version tools.