Creating Apps
Complete guide to building apps with actions, webhooks, OAuth, and the full tools API
Apps are TypeScript modules that integrate external services into your
workspace. They can define actions callable from chat and flows, receive
webhooks, sync data on a schedule, authenticate via OAuth, and render UI. This
guide covers the full createApp API and every tool available to your app code.
App structure
Every app is a single file that default-exports a createApp() call:
Users invoke actions from chat (/my-app do something) or from
flows as pipeline steps.
App definition
The createApp function accepts an AppDefinition object:
| Field | Type | Description |
|---|---|---|
id | string | unique identifier, used in flow steps and URLs |
name | string | display name |
description | string | shown in the marketplace and to the AI |
version | string | semver string |
icon | string | Lucide icon name or path to an image file |
theme | string | color theme for UI accents (blue, green, orange, etc.) |
options | object | configurable options schema (see below) |
optionsDefaults | object | default values for options |
optionsDescriptions | object | help text for each option |
optionsRequired | object | which options are required |
oauth | object | OAuth configuration |
perUser | boolean | whether the app stores data per-user |
events | object | declarative webhook events the app emits |
outputs | object | describes the shape of data the app produces |
api | object | API configuration for strategy-driven actions |
helpers | object | pure functions available in strategy-generated code |
actions | object | named actions the app exposes |
onWebhook | function | handler for incoming webhook payloads |
onAuthSuccess | function | runs after successful OAuth |
suggestedFlowsAndBlocks | array | suggested flow/block threads created on install |
frontend | function | React component for the app UI |
Options
Options let server admins configure your app at install time. Define them as a
schema of 'string' | 'number' | 'secret' fields:
Secret options are stored server-side only and never synced to clients. They
render as a password field in the UI. At runtime, secret values are resolved
and merged into options just like string and number fields.
Options are available in every action and backend run via options:
API configuration
If your app talks to an external API, the api field tells the platform how to
make requests on your behalf. This is especially important for strategy-driven
actions (see below), where the platform uses your API docs to generate the right
calls automatically.
| Field | Type | Description |
|---|---|---|
base | string | base URL for the external service API. supports {{option}} template vars that resolve from your app’s configured options |
auth | string | authentication type: 'bearer' (Authorization header), 'basic' (Basic auth), or 'custom' (you handle it in headers) |
docs | string | natural language API documentation that guides how the app interacts with the service |
The docs field is the most important part — it tells the platform what
endpoints exist, what parameters they accept, and what they return. Write it in
plain English or as a concise endpoint reference. The better your docs, the more
reliably strategy-driven actions will work.
Helpers
Pure functions you want available inside strategy-generated code. Useful for date formatting, unit conversion, data transformation, or any reusable logic that doesn’t make API calls:
Each helper is a string containing a JS function expression. Helpers are
injected as top-level variables in generated snippets alongside fetch,
store, stat, and other built-ins. Keep them pure — no API calls, no side
effects, just transforms.
Actions
Actions are the primary way apps expose functionality. They can be invoked from
chat, flows, and the AI agent. Each action has a description (used by the AI
to decide when to call it) and optionally a run function.
Action properties
| Field | Type | Description |
|---|---|---|
description | string | what the action does (shown to AI and users) |
noun | string | what the action returns (e.g. “issues”, “response”) |
messageFormat | string | describes expected input shape |
requiresInput | boolean | whether the action needs input to run |
synonyms | string[] | alternate terms that help match user instructions |
runsOn | 'server' | 'desktop' | where the action executes |
run | function | the action implementation (omit for strategy-driven) |
When runsOn is 'desktop', the action executes on the user’s machine via the
desktop app instead of in the cloud sandbox. This unlocks tools.files for
local filesystem access, which is useful for scanning directories, exporting
data to local files, or reading local configuration. Desktop actions can’t be
triggered by scheduled flows — they require an active desktop session.
Strategy-driven vs imperative actions
Actions come in two flavors:
-
Strategy-driven — omit the
run()function. The platform uses AI to generate code that calls your app’s API based on theapi.docs,outputs, and the user’s instruction. The generated code is cached after the first run, so subsequent calls are fast and deterministic. This is the preferred approach for flexible API interactions where the user might ask for anything. -
Imperative — provide a
run()function with your own logic. Use this for deterministic operations like syncing data, managing webhook subscriptions, or anything where you need precise control over the behavior.
Synonyms
Synonyms are alternate terms that help the platform route user instructions to the right action. They’re matched against the user’s input to find the best action to run.
If not provided, synonyms are auto-generated from the action name and
description at build time. Set synonyms: [] to explicitly opt out of fuzzy
matching — useful for catch-all actions like query that should only match
when no other action does.
Run context
The run function receives:
| Property | Description |
|---|---|
input | the user’s input (see below) |
context | channel/server context |
options | configured app options |
tools | the full tools API |
controller | AbortController for cancellation |
oauthToken | OAuth token if the app uses OAuth |
Input object
The input argument contains the user’s input parsed into several forms:
In a flow pipeline like github.sync tamagui/tamagui | notify.default, the
notify step receives the output of github.sync as input.summary.
Action context
Return values
Actions can return a string, an AppResult object, or void:
The summary field is the human-readable string displayed to users and piped to
downstream steps. The data array contains structured AppResultDataItem
objects with well-defined fields the UI can render automatically — titles as
text, statuses as badges, urls as links, kinds for grouping. Any extra fields on
the returned object are merged into the pipeline’s context object. The data
array flows forward as the next step’s input.data.
outputs are for deferred chat-side effects the runtime can execute after the
action returns, such as posting messages, reacting, or pinning. Notifications
and other UI feedback should be done imperatively from app code with tools like
tools.notify, not returned through outputs.
Data items have these fields:
| Field | Type | Description |
|---|---|---|
type | string | item type (e.g. “payment”, “issue”) |
key | string | unique key for deduplication |
value | any | domain-specific payload |
title | string | human-readable title |
status | string | status string (rendered as badge) |
kind | string | kind for grouping/filtering |
url | string | external link (rendered as clickable) |
tags | string[] | tags for filtering |
externalId | string | ID in the external system |
Returning data items does not automatically persist them to the data store —
use tools.data.public.set() for that. Data items are the structured “view” of
what the action produced, consumed by the pipeline, blocks, and the UI.
The query action
An action named query is the generic ask-anything entrypoint for an app. It
is used for ad-hoc natural-language requests and runs when the app is called
without a more specific action name:
Tools API
Every action, backend, and webhook handler receives a tools object with the
full platform API.
tools.data
Store and retrieve structured data. Both tools.data.public and
tools.data.private share the same interface.
- public data is visible to all server members and searchable by the AI
- private data is scoped to the app install (sync cursors, tokens, state)
set(key, value, meta?) stores a key/value pair. The value should be a string
(JSON-stringify objects). The optional meta object supports:
| Field | Type | Description |
|---|---|---|
title | string | human-readable title |
kind | string | data kind for filtering |
status | string | status indicator |
tags | string[] | tags for filtering |
weight | number | sort weight |
format | string | 'text' | 'json' | 'markdown' |
url | string | external URL |
externalId | string | ID in the external system |
startsAt | number | start timestamp |
endsAt | number | end timestamp |
createdAt | number | creation timestamp |
updatedAt | number | last update timestamp |
syncedAt | number | last sync timestamp |
aiVisibility | string | 'context' | 'searchable' | 'hidden' |
scope | string | 'user' | 'server' | 'channel' |
extra | object | arbitrary extra metadata |
get(key) returns a Datum object { key, value, title, kind, ... } or
null.
getAll(query?) returns an array of all data entries. The optional query object filters results:
| Field | Type | Description |
|---|---|---|
kind | string | filter by kind |
status | string | filter by status |
tags | string[] | filter by tags |
limit | number | max results |
sort | string | 'newest' | 'oldest' |
delete(key) removes an entry by key.
stat(key, value, meta?) stores a point-in-time stat (counts, gauges, current values). Handy for dashboard blocks:
series(name, value, options?) stores a time-series data point with auto-aggregation. Deduplicates within the period and computes running aggregates (avg, sum, min, max, count):
tools.message
Post messages to channels:
React to a message with an emoji:
Pin or unpin a message (toggles):
tools.notify
Imperatively create notifications in the workspace. This is the correct path
for durable alerts; do not return notifications through outputs:
Send a push notification to a specific user:
tools.generate
Generate text or structured data with AI. Always calls the model — never cached. Use for creative or dynamic tasks where caching doesn’t make sense.
Model tiers: 'xxs', 'xs', 'sm', 'md', 'lg', 'xl'. xxs is
the cheapest option, good for simple extraction and classification tasks. xl
uses the most capable models for complex reasoning. You can also pass a specific
model ID string.
tools.abort
Stop the current flow pipeline. Throws immediately and skips remaining steps:
tools.progress
Update a progress indicator visible in the app UI (0-100):
tools.webhooks
Webhook subscriptions are managed through flow event triggers. When a flow uses
on app.event, the platform automatically registers and manages the webhook
with the external service. See When Flows Run
for details on event triggers.
Apps declare the events they emit in their events definition, and flows listen
with on app.event syntax. The onWebhook handler on the app processes
incoming payloads and returns a matching event name.
tools.flows
Programmatically create and delete flows:
tools.helpers
tools.crypto
Sign JWTs and encode data:
tools.strategy
Learn on first run, execute deterministically after. On the first run, the strategy system uses AI to generate a JS snippet that maps input to output. On subsequent runs, the cached snippet executes instantly — no AI call, same result. Self-healing: if the cached code fails, it regenerates with error context.
Three methods cover all use cases:
Strategies must be time-independent — cache the shape of an operation (relative period types, field names), not concrete timestamps. Resolve time-dependent values at runtime in your action code.
See the tools reference for the full strategy API.
tools.files (desktop only)
File system access scoped to ~/.start/{serverSlug}/apps/{appSlug}/files/. Only
available in actions with runsOn: 'desktop'.
OAuth
Connect your app to third-party services with OAuth. Declare the provider and scopes:
After OAuth, the token is available as oauthToken in every run context.
Destructure it directly — there’s no need to fetch it from the data store:
Webhooks
Apps can receive webhooks from external services. Declare the events your app emits, then handle incoming payloads:
Events declared in events can be used as flow triggers. For example, a flow
with on my-app.deploy triggers whenever the webhook fires with
event: 'deploy'.
Outputs
Describe the shape of data your app produces. Each key names a data type and maps to a simple schema of its fields. This helps the flow runner and AI understand what your actions return, and for strategy-driven actions, it guides the generated code to produce correctly shaped results.
Types use a simple JSON schema: 'string', 'number', 'boolean', or prefix
with ? for optional fields.
Suggested Flows And Blocks
Create suggested automation threads when the app is installed:
You can also suggest blocks in their own threads:
Frontend
Apps can render a full UI panel via the frontend function. It receives the
app’s configured options and the tools object:
The frontend function signature:
Use options to display current configuration and tools for data access
(tools.data.public.getAll(), etc.). The frontend renders in the apps panel and
can display alongside your blocks.
Sandbox environment
Backends and actions run in a sandboxed isolate with these globals available:
fetch, setTimeout, clearTimeout, URL, URLSearchParams, console,
Buffer, TextEncoder, TextDecoder, btoa, atob, and the Web Crypto API.
No direct filesystem or network access beyond fetch. Memory is capped at 128
MB and execution times out after 60 seconds.
Complete example
An app that monitors endpoint health, stores results, and notifies on failures:
Publishing to the marketplace
Once your app is ready, you can publish it to npm and submit it for listing in the start.chat marketplace.
1. Scaffold and build
Terminal
2. Publish to npm
Terminal
This builds the app, publishes to npm, and registers it with start.chat. Use
--dry-run to validate without publishing.
3. Submit for marketplace listing
Terminal
Once submitted, the app enters a review queue. Approved apps appear in the marketplace with a verified badge. See the CLI reference for app install and app submit for full command details.