Skip to content

Creating Custom Apps

Custom apps let you extend what your agents can do by writing code that runs in a secure sandbox. You do not need a separate development environment or deployment pipeline — everything happens inside the Sprigr dashboard using a full-featured IDE with multi-file support.

This guide walks you through creating an app from scratch, testing it, and deploying it so your agents can use it.

  1. 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.

  2. 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_check or invoice_lookup. Must be lowercase with underscores.
    • 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 outbound-request allowlist.

    Submitting creates the app at version 0.1.0.

  3. 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.ts is 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.
  4. Write your handler code

    Refine your app’s logic. See the sections below for details on the handler function and the app runtime.

  5. Define the input schema

    Switch to the Schema tab and define what inputs your app expects. See the input schema section below.

  6. 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.

  7. Deploy

    When your app is working correctly, click Deploy. This bundles all your files using esbuild, increments the version number, and makes the new version available to agents.

For anything beyond a simple utility, you will want to split your code across multiple files. The IDE supports a file tree where you can add, rename, and delete modules:

my-app/
index.ts ← Main handler (entry point)
api-client.ts ← Reusable API wrapper
formatters.ts ← Data formatting helpers
types.ts ← Shared type definitions

Your entry point (index.ts) must export a default handler function. Other files can export utilities that the handler imports. When you deploy, all files are bundled into a single JavaScript file using esbuild — the platform handles the bundling automatically.

api-client.ts
export 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.ts
import { fetchWeather } from './api-client';
export default async function handler(input, ctx) {
const data = await fetchWeather(input.postcode, ctx.auth.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
}))
};
}

Every tool exports a single async handler function as its default export. The function receives the validated input as its first argument and a context object as its second, and returns data that the agent can use in the conversation.

export default async function handler(input, ctx) {
const response = await fetch(
`https://api.weatherapi.com/v1/forecast.json?key=${ctx.auth.api_key}&q=${input.postcode}&days=3`
);
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,
minTemp: day.day.mintemp_c,
chanceOfRain: day.day.daily_chance_of_rain
}))
};
}

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.

Inside your handler you can use:

Make HTTP requests with the standard fetch() API or the provided http.request(url, options) helper. All outbound requests are restricted to domains you have declared in your app’s network allowlist. Requests to unlisted domains are blocked at runtime.

Credentials are supplied by each installing company at install time, stored encrypted, and injected at runtime — they never appear in your code or logs. Auth fields the app declares arrive on the handler’s context argument:

const apiKey = ctx.auth.api_key;

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.

Per-installation configuration. If your app declares a config schema, each installing company can override the defaults, and your handler reads the merged result:

const region = await config.get("region");
const everything = await config.getAll();

The composition API for calling other installed tools from within your app:

const weather = await tools.call("weather_check", { postcode: "4220" });
const available = await tools.list(); // Returns array of available tool names

See the tool composition section below for details and limits.

Apps can opt in to a private search index for caching data per installation. When declared, your handler can import objects into it, search it, and delete from it — useful for keeping a fast local copy of slow upstream data.

The Schema tab uses JSON Schema to define what inputs your app 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"]
}

The Test tab lets you run your app in a sandboxed environment before deploying it to production. This means you can iterate on your code without affecting live agents.

  1. Input JSON — Paste or type the input your app should receive. It must match the schema you defined.
  2. Test secrets — Enter temporary secret values for testing. These are not stored and are only used for the current test run.
  3. Run Test — Click Run Test to execute your handler. The output panel shows the run status, any console.log() output, and the return value.

When you click Deploy, three things happen:

  1. 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.
  2. Version increment — The app’s version number is automatically incremented (0.1.0, 0.1.1, and so on).
  3. 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.

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). Simply 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. The agent-created apps appear in the same Apps page and can be edited in the IDE like any other app.

Apps can call other installed tools using the tools.call() function. This lets you build complex capabilities from smaller, reusable pieces.

export default async function handler(input) {
// Get weather forecast
const weather = await tools.call("weather_check", {
postcode: input.jobPostcode
});
// Check if conditions are suitable for outdoor work
const rainyForecast = weather.forecast.some(
day => day.chanceOfRain > 70
);
if (rainyForecast) {
return {
recommendation: "postpone",
reason: "High chance of rain in the forecast period",
weather: weather.forecast
};
}
// Find available slots via another tool
const slots = await tools.call("calendar_availability", {
startDate: input.preferredDate,
duration: input.estimatedHours
});
return {
recommendation: "schedule",
availableSlots: slots,
weather: weather.forecast
};
}

If your app relies on specific platform integrations (like Gmail or simPRO), you can declare them as dependencies in the app’s manifest:

  • Required integrations — Installation is refused when the integration is missing, so the app can never end up installed but broken.
  • Optional integrations — The app installs without these but gains extra functionality when they are available. Your handler code should check whether the integration is connected and handle both cases.

Apps can also declare dependencies on specific tools from other marketplace apps or from your connected integrations. These are surfaced on the install consent screen and become grants the installing organisation controls — see App Permissions and Grants.

In the Settings tab, declare every external domain your app needs to reach. For example:

  • api.weatherapi.com
  • api.xero.com
  • hooks.slack.com

Any fetch() call to a domain not on your list will be blocked. This protects against accidental data exfiltration and ensures apps only communicate with intended systems.

Here is a complete example that fetches a weather forecast and formats it for field service scheduling decisions:

export default async function handler(input, ctx) {
const response = await fetch(
`https://api.weatherapi.com/v1/forecast.json?key=${ctx.auth.api_key}&q=${input.postcode}&days=${input.days || 3}`
);
if (!response.ok) {
throw new Error(`Weather API returned ${response.status}`);
}
const data = await response.json();
return {
location: data.location.name,
state: data.location.region,
forecast: data.forecast.forecastday.map(day => ({
date: day.date,
condition: day.day.condition.text,
maxTemp: day.day.maxtemp_c,
minTemp: day.day.mintemp_c,
chanceOfRain: day.day.daily_chance_of_rain,
maxWind: day.day.maxwind_kph,
suitableForOutdoorWork:
day.day.daily_chance_of_rain < 40 &&
day.day.maxtemp_c < 40 &&
day.day.maxwind_kph < 50
}))
};
}

With the input schema:

{
"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"]
}

The in-dashboard IDE covers single-tool and small multi-file apps. Full marketplace apps can go much further: a manifest can declare multiple tools, install-time secrets, inbound webhooks, cron schedules, long-running durable jobs, messaging channels, per-install database migrations, and workflow templates. These apps are authored outside the dashboard and published through the platform build pipeline (via the Sprigr CLI or MCP), which builds and deploys the app per installation.

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 Publish lists it publicly in the marketplace. 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.