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.
Creating a new app
Section titled “Creating a new app”-
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.
-
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_checkorinvoice_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.”
- Name — A short, descriptive identifier like
-
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.
-
Write your handler code
Start writing your app’s logic. See the sections below for details on the handler function and available globals.
-
Define the input schema
Switch to the Schema tab and define what inputs your app 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. The test executes your code in a sandbox without deploying — check the console for logs and verify the output.
-
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.
Multi-file projects
Section titled “Multi-file projects”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 definitionsYour 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.
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.tsimport { 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 })) };}Writing the handler function
Section titled “Writing the handler function”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.
Available globals
Section titled “Available globals”Inside your handler function, you have access to four global objects:
fetch()
Section titled “fetch()”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.
secrets
Section titled “secrets”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;sprigr
Section titled “sprigr”The platform API object for interacting with Sprigr Teams features:
sprigr.company.id— The ID of the company running this appsprigr.agent.id— The ID of the agent that invoked this appsprigr.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 namesSee the tool composition section below for details and limits.
Defining the input schema
Section titled “Defining the input schema”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"]}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. This means you can iterate on your code 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 — Click Run to execute your handler. The console shows any
console.log()output, and the result panel shows 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 (1.0.0, 1.1.0, and so on).
- 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.
Agents can create apps too
Section titled “Agents can create apps too”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.
Tool composition
Section titled “Tool composition”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 };}Declaring dependencies
Section titled “Declaring dependencies”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.
Network domain allowlist
Section titled “Network domain allowlist”In the Settings tab, declare every external domain your app needs to reach. For example:
api.weatherapi.comapi.xero.comhooks.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.
Example: Weather check app
Section titled “Example: Weather check app”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"]}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, 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.