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 status and version numbers.

  2. Click New App

    Click the New App button in the top right. You will be asked for a name and description.

    • Name — A short, descriptive identifier like weather_check or invoice_lookup. Agents see this name when deciding which tool to use, so make it clear. Use snake_case.
    • Description — A plain-language explanation of what the app does and when an agent should use it. 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.”
  3. The App IDE opens

    After naming your app, the IDE opens with a full development environment:

    • File tree (left sidebar) — Manage multiple files in your app. Create, rename, and delete files to organise your code into logical modules.
    • Code editor (centre) — A Monaco-based editor with syntax highlighting, autocomplete, and error markers. Open files appear as tabs so you can switch between them.
    • Tabs panel (right) — Three tabs: Schema (define inputs), Test (run your app), and Settings (metadata, tags, domains).
    • Console output (bottom) — Shows logs from console.log() calls, errors, and network request traces when you run tests.
  4. Write your handler code

    Start writing your app’s logic. See the sections below for details on the handler function and available globals.

  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. The test executes your code in a sandbox without deploying — check the console for logs and verify the output.

  7. Deploy

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

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

my-app/
index.ts ← Main handler (entry point)
utils/
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.

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

Every app exports a single async handler function as its default export. The function receives the validated input and returns data that the agent can use in the conversation.

export default async function handler(input) {
const response = await fetch(
`https://api.weatherapi.com/v1/forecast.json?key=${secrets.WEATHER_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 function, you have access to four global objects:

The standard Fetch API for making HTTP requests. All outbound requests are restricted to domains you have declared in your app’s network allowlist. Requests to unlisted domains are blocked at runtime.

An object containing the secret values configured for this app installation. Use this for API keys, tokens, and credentials. Secrets are encrypted at rest and injected at runtime — they never appear in your code or logs.

const apiKey = secrets.MY_API_KEY;

The platform API object for interacting with Sprigr Teams features:

  • sprigr.company.id — The ID of the company running this app
  • sprigr.agent.id — The ID of the agent that invoked this app
  • sprigr.conversation.id — The current conversation ID (if applicable)

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.

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 — Click Run to execute your handler. The console shows any console.log() output, and the result panel shows 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 (1.0.0, 1.1.0, and so on).
  3. Agent sync — All agents that have this app installed receive the updated version. They will use the new version on their next invocation (unless the installer has pinned to a specific version).

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 apps on your behalf using the manage_team tool with the create_custom_tool action. 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 Settings tab:

  • Required integrations — The app will not work without these. Installers are prompted to connect the integration before they can use the app.
  • Optional integrations — The app works without these but gains extra functionality when they are available. Your handler code should check whether the integration is connected and handle both cases.

Declaring dependencies ensures installers know what they need before installing, preventing broken apps that fail because a required integration is missing.

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) {
const response = await fetch(
`https://api.weatherapi.com/v1/forecast.json?key=${secrets.WEATHER_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"]
}

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, change its trust tier to Shared or Listed in the Settings tab. 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.