Tools API

The complete tools API available to app actions, backends, and webhook handlers

Every action, backend, and webhook handler receives a tools object with the full platform API. This page is the complete reference for all available tools.

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 a value with metadata
await tools.data.public.set('customer:123', JSON.stringify(customer), {
title: customer.name,
kind: 'customer',
status: 'active',
tags: ['enterprise'],
url: 'https://example.com/customer/123',
})
// get a single value
const item = await tools.data.public.get('customer:123')
// returns: { key, value, title, kind, status, tags, ... }
// query with filters
const active = await tools.data.public.getAll({
kind: 'customer',
status: 'active',
sort: 'newest',
limit: 50,
})
// delete
await tools.data.public.delete('customer:123')

set(key, value, meta?) stores a key/value pair. The value should be a string (JSON-stringify objects). The optional meta object supports:

FieldTypeDescription
titlestringhuman-readable title
kindstringdata kind for filtering
statusstringstatus indicator
tagsstring[]tags for filtering
weightnumbersort weight
formatstring'text' | 'json' | 'markdown'
urlstringexternal URL
externalIdstringID in the external system
startsAtnumberstart timestamp
endsAtnumberend timestamp
createdAtnumbercreation timestamp
updatedAtnumberlast update timestamp
syncedAtnumberlast sync timestamp
aiVisibilitystring'context' | 'searchable' | 'hidden'
scopestring'user' | 'server' | 'channel'
extraobjectarbitrary 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:

FieldTypeDescription
kindstringfilter by kind
statusstringfilter by status
tagsstring[]filter by tags
limitnumbermax results
sortstring'newest' | 'oldest'

delete(key) removes an entry by key.

stat(key, value, meta?) stores a point-in-time metric. The value is a record with numeric fields. Updating the same key overwrites the previous value. Use this for current counts, gauges, and snapshots:

await tools.data.public.stat(
'github-stats',
{
openIssues: 42,
openPRs: 7,
contributors: 128,
},
{ kind: 'stat', title: 'GitHub Stats' },
)

series(name, value, options?) stores a time-series data point with automatic aggregation. Each call creates a period-keyed entry and updates running aggregates (sum, count, min, max, avg) plus a rolling points array (up to 30 values) on the aggregate row. Use this for tracking values over time:

await tools.data.public.series(
'daily-revenue',
{
value: 1250.0,
currency: 'USD',
},
{ period: 'daily' },
)

The period option controls bucketing: 'hourly', 'daily' (default), 'weekly', or 'monthly'. Within each period, updating the same series replaces the value and recalculates aggregates.

The aggregate row (key = series name, type = series-agg) stores running stats and the points array. Chart blocks can load this single row via previewQuery="daily-revenue" for sparkline rendering without querying all individual series entries. For multi-field values, points is an object keyed by field name.

See Data for the full data model and querying patterns.

tools.message

Post messages to channels:

await tools.message.insert({
content: 'deployment succeeded',
channelId: context.channelId,
serverId: context.serverId,
threadId: context.threadId, // optional, replies in a thread
})

React to a message with an emoji:

await tools.message.react({
messageId: 'msg_abc',
emoji: 'thumbsup', // emoji keyword or unicode value
})

Pin or unpin a message (toggles):

const result = await tools.message.pin({
messageId: 'msg_abc',
channelId: context.channelId,
serverId: context.serverId,
})
// result: { pinned: true } or { pinned: false }

tools.notify

Imperatively create notifications in the workspace. Use this for durable alerts instead of returning notification-like values through outputs:

// in-app notification (info by default)
tools.notify('sync completed')
tools.notify('rate limit hit', 'warn')
tools.notify('sync failed', 'error')

Send a push notification to a specific user:

// by user ID or @username
await tools.notify.push('user-id-or-@username', 'you have a new assignment')
// with title and body
await tools.notify.push('@alice', {
title: 'Deploy Complete',
body: 'v2.1.0 is live on production',
})

tools.generate

Uncached AI generation. Always calls the model — use sparingly for creative or dynamic tasks where caching doesn’t make sense. For deterministic extractions that repeat across runs, use tools.strategy instead.

text(tier, prompt)

Generate a text response:

const summary = await tools.generate.text('md', `Summarize: ${text}`)

object(tier, prompt, schema?)

Generate a structured JSON object. The optional schema constrains the output shape:

const entities = await tools.generate.object(
'sm',
`Extract people and places from: ${text}`,
{ people: ['string'], places: ['string'] },
)
// → { people: ["Alice", "Bob"], places: ["NYC"] }

Model tiers: 'xxs', 'xs', 'sm', 'md', 'lg', 'xl'. Use 'xxs' for lightweight tasks like categorization, extraction, and filtering. '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:

const data = await tools.data.private.get('api-key')
if (!data) {
tools.abort('no API key configured')
}

tools.progress

Update a progress indicator visible in the app UI (0-100):

tools.progress(0)
for (let i = 0; i < items.length; i++) {
await processItem(items[i])
tools.progress(Math.round(((i + 1) / items.length) * 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:

// create a flow from pipedown syntax
const { id } = await tools.flows.create(` every 1 hour /github sync /notify push sync complete `)
// delete a flow (only flows created by this app)
await tools.flows.delete(id)

tools.helpers

const slug = tools.helpers.slugify('Hello World') // 'hello-world'
const id = tools.helpers.randomId() // random unique string

tools.crypto

Sign JWTs and encode data:

// sign a JWT (RS256)
const token = await tools.crypto.signJWT(
{ iss: 'my-app', sub: 'user-123' },
privateKeyPEM,
{ expiresIn: '10m' },
)
// base64url encode
const encoded = tools.crypto.base64url('hello')

tools.strategy

Cached AI that learns on first run and replays deterministically after. All strategy methods generate a JS snippet on first run, cache it per step, and execute it instantly on subsequent runs. Self-healing: if the cached code fails, it regenerates with error context and retries.

Core principle: strategies must be time-independent

Strategies are cached and reused across runs. Never put timestamps, dates, or any time-dependent values into a strategy. The strategy should capture the shape of the operation, and the app computes concrete values at runtime.

// BAD — timestamps get cached, stale on next run
const parsed = await tools.strategy.object({
task: `Parse: "today"\nCurrent time: ${new Date().toISOString()}`,
schema: { created: { gte: 'unix_ts', lte: 'unix_ts' } },
})
// GOOD — relative type is cached, timestamps computed at runtime
const parsed = await tools.strategy.object({
task: `Parse: "today" into a relative period type`,
schema: { relative: '"today"|"yesterday"|"last-n-days"', n: '?number' },
})
const params = resolveAtRuntime(parsed, new Date())

object(opts)

Extract a structured object from the step’s input context. The generated JS receives { instruction, summary, data } and returns an object matching the schema.

const params = await tools.strategy.object({
task: `Parse this Stripe payments query: "${query}"\n- "today" = period "day"\n- "this week" = period "week"`,
schema: { period: '"all"|"day"|"week"|"month"|"year"', n: '?number' },
})
// → { period: "week" }
const apiParams = await tools.strategy.object({
task: `Determine GitHub API endpoint for: "${input.instruction}"\nAvailable repos: ${repos.join(', ')}`,
schema: { pathname: 'string', method: 'string', params: 'object' },
})
// → { pathname: "/repos/acme/api/issues", method: "GET", params: { state: "open" } }

Options:

FieldTypeDescription
taskstringwhat the strategy needs to figure out
schemaobjectexpected output shape (optional)
modelOrTierModelOrTiermodel tier (default: 'xxs')
forceRegeneratebooleanregenerate even if cached

list(opts)

Same as object() but the generated JS returns an array. Schema describes the shape of each item.

const steps = await tools.strategy.list({
task: 'break deployment plan into ordered steps',
schema: { name: 'string', command: 'string' },
})
// → [{ name: "build", command: "npm run build" }, ...]

Takes the same options as object().

data(opts)

Map raw API data to standardized AppResultDataItem format. Generates a JS function that receives the raw data and returns { summary, items, count, meta? }. Items conform to AppResultDataItem — ready to return from your action as data: result.items.

const raw = await stripeAPI('/charges', params)
const result = await tools.strategy.data({
data: raw,
task: 'stripe payments',
fields: ['amount', 'customer_email', 'status'],
})
return { summary: result.summary, data: result.items }

Options:

FieldTypeDescription
dataunknownraw API response data
taskstringwhat this data represents (optional)
fieldsstring[]fields to prioritize in mapping (optional)
modelOrTierModelOrTiermodel tier
forceRegeneratebooleanregenerate even if cached
maxRetriesnumberretry attempts if generated code fails (default: 2)
hintsobjectcontext hints (see keyFormats below)

Key format hints: pass hints: { keyFormats: { issue: 'github-issue:{number}' } } to constrain item keys. This ensures sync, actions, and webhooks all write to the same data row for a given entity. The generated JS will use these exact key patterns.

const result = await tools.strategy.data({
data: rawIssues,
task: 'github issues',
hints: { keyFormats: { issue: 'github-issue:{number}', pr: 'github-pr:{number}' } },
})

Return shape:

FieldTypeDescription
summarystringhuman-readable summary
itemsAppResultDataItem[]standardized data items
countnumberalways items.length
metaobjectoptional aggregates and computed fields

The generated JS snippet maps API-specific fields to the well-defined AppResultDataItem shape (type, key, value, title, status, kind, url, tags, externalId). The UI can render these automatically as tables, lists, badges, and links without knowing anything about the source API.

Design guidelines

  1. Cache the shape, not the data. Strategies are saved and reused — they must produce the same correct result regardless of when they run.
  2. Use relative descriptions for time. Instead of Unix timestamps, use types like "today", "last-n-days", "this-month" and resolve to timestamps in your action code at runtime.
  3. Strategies generate once, run forever. Think of them as learning a deterministic extraction plan on first run. If the plan depends on the current date, it will be wrong on the second run.
  4. Use forceRegenerate sparingly. Only when you know the cached strategy is invalid (e.g., schema changed). Flow-level strategy clearing handles most recovery.

strategy.fetch()

Combines strategy.object() + fetch in one call:

const result = await tools.strategy.fetch({
task: 'determine GitHub API path and query params',
schema: { owner: 'string', repo: 'string', state: '"open"|"closed"|"all"' },
fetch: (params) =>
githubFetch(`/repos/${params.owner}/${params.repo}/issues`, {
state: params.state,
}),
})

The strategy determines the API parameters once (cached). Every subsequent run skips the AI and goes straight to the fetch. See Strategies for more on caching, self-healing, and time-independence.

tools.files (desktop only)

File system access scoped to ~/.start/{serverSlug}/apps/{appSlug}/files/. Only available in actions with runsOn: 'desktop'.

await tools.files.write('config.json', JSON.stringify(config))
await tools.files.writeJSON('config.json', { key: 'value' })
const content = await tools.files.read('config.json')
const data = await tools.files.readJSON('config.json')
const exists = await tools.files.exists('config.json')
const files = await tools.files.list('/', {
recursive: true,
pattern: '*.json',
})
await tools.files.delete('config.json')
const abs = tools.files.resolve('config.json')