Documentation Index
Fetch the complete documentation index at: https://docs.useterse.ai/llms.txt
Use this file to discover all available pages before exploring further.
Terse workflows combine two sources:
- the public
terse-sdk package
- the generated helpers in
src/terse.generated.ts
After terse generate, integration triggers and skills are grouped under top-level Triggers and Skills objects (for example Triggers.github.onPROpened(...)). See the Triggers and Skills reference for every helper.
Core SDK imports
Import these from terse-sdk:
createJob, CreateJobParameters: register a workflow (call at module top level or from imported modules)
generateText: the shorthand for agentic runs — one prompt in, final output out, with the model free to call any tool granted via skills. Use this for almost everything; see generateText below
Terse, TERSE_JOB_WEBHOOK_TRIGGER_PATH: client and mount path for handleTrigger when you self-host the data plane webhook endpoint
TerseAgent, EventType: the lower-level agent that generateText wraps. Reach for TerseAgent.create() directly only when you need to stream with run() or reuse one agent instance across calls; EventType enumerates values on streamed results
runWithJobContext, getJobContext, TerseJobContext: wrap a handler with runWithJobContext to preserve run attribution. Outside of it, set TERSE_BACKEND_URL if needed (default https://api.useterse.ai) and optionally TERSE_RUN_ID
formatTriggerForAgent, debugTrigger: helpers for plain trigger payloads
Re-exported types
terse-sdk re-exports types you may need when annotating handlers or consuming run history. You don’t need to import these unless you’re typing data the SDK doesn’t return for you.
| Group | Types |
|---|
| Base event interface | Trigger |
| System trigger event types | CronTrigger, ManualSampleTrigger, WebhookTrigger<BodyType>, WebMonitorTrigger<TStructured>, WebMonitorTriggerFor<TSchema> |
| Integration trigger event types | GithubTrigger, GithubPRTrigger, AttioTrigger, AttioRecordCreatedTrigger, HeyReachTrigger, HeyReachMessageReplyReceivedTrigger, and similar per-integration types. See Triggers for the full set. |
| Integration event-type enums | SlackEventType, GitHubEventType, LinearEventType, GmailEventType, WorkOSEventType, HeyReachEventType, AttioEventType, FrequencyUnit |
| Run history (Activity, API payloads) | RunHistoryRecord, RunHistoryStatus, RunHistoryTrigger, RunHistoryDecision, RunHistoryAction |
For TypeScript workflows that use structured output schemas, also install and import zod:
npm install terse-sdk zod
Authentication and TERSE_API_KEY
Terse, TerseAgent, and other SDK calls that reach Terse send Authorization: Bearer … from process.env.TERSE_API_KEY. Terse API tokens use the terse_ prefix and come in two flavors.
User tokens are the API tokens you create in the Terse app or get from terse login. Use them for local runs, the CLI, and self-hosted data plane handleTrigger servers. They authenticate to the full set of SDK routes your workflow needs, including deploy, codegen, and runtime calls.
Project-scoped tokens are minted automatically inside the Terse Cloud data plane’s Modal sandboxes. They are limited to runtime endpoints for that project (agent runs, tool execution, approvals, and session streams) and cannot stand in for a user token on organization or integration-management routes. The control plane removes the sandbox token when the run completes.
Tokens with an expiration stop working when they expire; the API responds with 401 and an expired-token message.
The API token list in the app shows user tokens only. Short-lived project tokens do not appear there.
Trigger payloads
Exported trigger types extend the canonical trigger object with two methods used everywhere Terse turns an event into prompts or logging:
formatForAgentRunner() returns a string to include in agent prompts (matches how the platform formats the event for the model)
debugLog() returns a one-line description for logs and CLI sample lists
Your onTrigger and filter callbacks receive these enriched objects, so you can call event.formatForAgentRunner() directly. For plain Trigger values (for example in tests), use formatTriggerForAgent(event) and debugTrigger(event) from terse-sdk.
createJob(...)
Registers a workflow at load time. The CLI imports your entry file (src/terse.jobs.ts by default), so every createJob() call that runs during that import is included. Split definitions across files with side-effect imports (for example import "./jobs/myWorkflow") if you prefer.
Duplicate name values throw an error when the second job registers. At most one webhook trigger is allowed per workflow.
| Field | Type | Description |
|---|
name | string | Unique workflow identifier. Used to match workflows across deploys and in terse test. |
triggers | typed trigger array | One or more triggers that start the workflow. |
filter | (event) => boolean | Promise<boolean> | Optional function to skip the run for specific events. See filtering events. |
onTrigger | (event) => Promise<void> | The workflow handler. Called once per trigger event. |
To point the SDK at a self-hosted control plane, set TERSE_BACKEND_URL (it defaults to https://api.useterse.ai). To route execution to a self-hosted data plane, set remoteServerUrl in terse.config.json instead.
Pass skills, toolApprovals, and the agent prompt to TerseAgent.create() inside onTrigger, not on createJob.
import { GithubPRTrigger, TerseAgent, createJob } from "terse-sdk"
import { Repos, Skills, SlackChannel, Triggers } from "./terse.generated"
createJob({
name: "pr-auto-reply",
triggers: [Triggers.github.onPROpened({ repo: Repos.MyOrg.MyRepo })],
onTrigger: async (event: GithubPRTrigger) => {
const agent = TerseAgent.create({
prompt: "You summarize pull requests and post concise thread replies in Slack.",
skills: [Skills.github({ repos: [Repos.MyOrg.MyRepo] }), Skills.slack({ channel: SlackChannel.Engineering })]
})
// workflow logic using agent and event
}
})
generateText
generateText is the shorthand for running the model. Give it a prompt and the skills the model is allowed to use; it runs a full agentic loop to completion and returns the final output. The model can call any tool granted via skills while it works. This is the call you reach for in almost every job.
import { generateText } from "terse-sdk"
import { AttioObject, Skills } from "./terse.generated"
const summary = await generateText({
prompt: `Score this account and explain why. ${event.formatForAgentRunner()}`,
skills: [Skills.attio({ object: AttioObject.Deal }), Skills.web()]
})
Pass an outputSchema (a zod schema) to get a typed, validated object back instead of a string. generateText is overloaded on it: with a schema the return type is z.infer<typeof schema>, without one it is string.
import { z } from "zod"
const scored = await generateText({
prompt: `Score this account and explain why. ${event.formatForAgentRunner()}`,
skills: [Skills.attio({ object: AttioObject.Deal })],
outputSchema: z.object({ score: z.number().min(0).max(100), rationale: z.string() })
})
// scored is typed as: { score: number; rationale: string }
| Field | Type | Description |
|---|
prompt | string | The instruction for the model. Include event context via event.formatForAgentRunner(). |
skills | skill array | Optional. Scopes which integration tools the model may call during the run. |
toolApprovals | string[] | Optional. Tool names that pause for human approval. |
outputSchema | zod schema | Optional. Returns a typed, validated object instead of a string. |
For deterministic calls (a fixed Slack message, a known field update), use toolbox.* instead — no agent needed. Between generateText for reasoning and toolbox for fixed actions, you rarely touch TerseAgent directly.
Terse
Use a Terse instance for handleTrigger, which verifies signed webhook payloads from the Terse backend and runs the matching registered workflow. You do not need new Terse() only to call createJob().
import { TERSE_JOB_WEBHOOK_TRIGGER_PATH, Terse } from "terse-sdk"
const terse = new Terse()
// Mount TERSE_JOB_WEBHOOK_TRIGGER_PATH on your HTTP server (Express, Hono, Next.js, etc.)
TerseAgent
TerseAgent is the lower-level primitive that generateText wraps. Reach for it directly only when you need something generateText doesn’t expose: streaming partial output with run(), or reusing one agent instance across several calls. Otherwise prefer generateText.
Create the agent inside onTrigger with TerseAgent.create(). Job context (session, run, API base URL) is picked up automatically from async context when the handler runs on the platform or via the CLI. The same context applies to TerseAgent.executeTool and to generated toolbox calls.
run(userMessage)
Streams the model run. Returns an async iterable of result objects (TextResult, FinalOutputResult, tool events, and so on).
import { EventType } from "terse-sdk"
for await (const chunk of agent.run("Summarize pipeline risk.")) {
if (chunk.type === EventType.TEXT) {
process.stdout.write(chunk.text)
}
}
Use run when you want to stream output progressively.
runAndWait(userMessage)
Waits for the model to complete and returns the final output string.
const output = await agent.runAndWait("Score the account and explain why.")
Use this form when you want raw text output.
runAndWait(userMessage, outputSchema)
Pass a zod schema to request structured output. The SDK sends the schema with the run, parses the final JSON, and validates it before returning.
import { z } from "zod"
const ScoreSchema = z.object({
score: z.number().min(0).max(100),
rationale: z.string()
})
const scored = await agent.runAndWait("Score this account and explain why.", ScoreSchema)
// scored is typed as: { score: number; rationale: string }
Static method. Calls a named tool directly, bypassing the LLM. Generated toolbox and agent.tools.* helpers use this path, so string-based and typed deterministic calls behave the same.
import { TerseAgent } from "terse-sdk"
import { SlackChannel } from "./terse.generated"
await TerseAgent.executeTool("slack_send_message", {
channelId: SlackChannel.DealDesk.channelId,
message: "Pipeline digest is ready."
})
The same tool accepts slackUserId (Slack member U…) to send a 1:1 DM; Terse opens the conversation if one does not exist yet. If you pass both channelId and slackUserId, the message is sent to channelId. With codegen, prefer SlackUser.*.userId from src/terse.generated.ts for member ids.
Use TerseAgent.executeTool when you want guaranteed execution of a specific tool by name (for example when the name is only known at runtime).
terse generate writes a toolbox export in src/terse.generated.ts with the same typed namespaces as agent.tools.*. Import toolbox to call integration tools deterministically without constructing a TerseAgent and without listing integrations in skills.
Generated helpers attach deterministic wrappers under agent.tools.*. These call integration actions directly, not through the LLM.
import { TerseAgent } from "terse-sdk"
import { Skills, SlackChannel } from "./terse.generated"
const agent = TerseAgent.create({
prompt: "You draft short Slack updates for the deal desk.",
skills: [Skills.slack({ channel: SlackChannel.DealDesk })]
})
await agent.tools.slack.sendMessage({
channelId: SlackChannel.DealDesk.channelId,
message: "Northstar Logistics: follow up on the open proposal thread."
})
Use agent.tools.* for guaranteed side effects when you already pass skills to TerseAgent.create(). The namespaces under agent.tools.* are filtered to the integrations in skills. For the same calls without listing skills, use the generated toolbox instead.
Structured output
Pass outputSchema to generateText (or runAndWait(message, schema) on a TerseAgent) when you need typed, validated JSON. Omit it when you need free-form text.
Filtering events
Use filter to skip runs for events that don’t match your criteria. Return true to run, false to skip.
type ContractPayload = { record: { values: { stage: string; company_name: string } } }
createJob({
name: "contract-alerts",
triggers: [Triggers.webhook.onRequest<ContractPayload>()],
filter: event => event.body.record.values.stage === "contract-sent",
onTrigger: async event => {
const agent = TerseAgent.create({
prompt: "You notify the deal desk when contracts are sent.",
skills: [Skills.slack({ channel: SlackChannel.DealDesk })]
})
await agent.tools.slack.sendMessage({
channelId: SlackChannel.DealDesk.channelId,
message: `Contract sent for ${event.body.record.values.company_name}.`
})
}
})
List tool names in toolApprovals on TerseAgent.create() to require human approval before those tools execute. During local testing, the CLI prompts in the terminal. In production, approval requests surface in the Terse app under Notifications.
createJob({
name: "slack-digest-with-approval",
triggers: [Triggers.slack.onMessage({ channel: SlackChannel.DealDesk })],
onTrigger: async event => {
const agent = TerseAgent.create({
prompt: "You draft a short reply when someone posts in Deal Desk.",
skills: [Skills.slack({ channel: SlackChannel.DealDesk })],
toolApprovals: ["slack_send_message"]
})
const summary = await agent.runAndWait(
["Summarize this thread in one sentence for the on-call owner.", `Channel: ${event.channelName}`, `From: ${event.userName}`, `Text: ${event.text}`].join("\n")
)
// Pauses until a human approves in the CLI (local) or Notifications (prod)
await agent.tools.slack.sendMessage({
channelId: SlackChannel.DealDesk.channelId,
message: `Suggested reply based on: ${summary}`
})
}
})
Use toolApprovals for workflows that write to production systems during early development, or when compliance requires a human in the loop.
Events
Event typing depends on the trigger.
| Trigger source | Event type |
|---|
| GitHub events | A Github*Trigger matching your helper, or GithubTrigger when using Triggers.github.trigger() |
| Slack events | SlackMessageTrigger, SlackAppMentionTrigger, SlackReactionAddedTrigger, or SlackTrigger (union) |
| Linear events | LinearIssueCreatedTrigger, LinearIssueUpdatedTrigger, LinearCommentCreatedTrigger, LinearTrigger |
| Gmail events | GmailTrigger |
| Attio events | The matching Attio*Trigger for your helper, or AttioTrigger when using Triggers.attio.trigger() |
| HeyReach outreach events | The matching HeyReach*Trigger for your helper, or HeyReachTrigger (union) |
| WorkOS events | The matching WorkOS*Trigger for your helper, or WorkOSTrigger (union) |
| Scheduled cron triggers | CronTrigger |
| Manual trigger | ManualSampleTrigger |
| Webhook HTTP requests | WebhookTrigger<TBody> |
| Web monitor | WebMonitorTrigger<TStructured> (use WebMonitorTriggerFor<typeof schema> when annotating manually) |
Trigger is the common base interface. Use the most specific trigger event type your trigger exposes. For webhooks, use Triggers.webhook.onRequest<YourBodyType>() from terse.generated so event.body matches the JSON you POST to the webhook URL.
Generated helpers
src/terse.generated.ts is created by terse generate. Do not edit it by hand.
It exports:
Triggers: per-integration trigger builders for every connected integration, plus the always-available Triggers.schedule, Triggers.webhook, and Triggers.webMonitor.
Skills: per-integration skill factories for every connected integration, plus the built-in Skills.web() and Skills.imageEdit().
- Workspace resource constants generated from your workspace (for example
Repos, SlackChannel, SlackUser, LinearTeam, LinearProject, NotionDatabase, PosthogProject, DatadogIndex, LaunchDarklyProject, HeyReachCampaign, and AttioObject.*).
toolbox: deterministic tool wrappers you can call without constructing an agent or listing skills.
agent.tools.*: the same wrappers attached to a TerseAgent, filtered by the skills you passed.
Re-run terse generate when your integration context changes. Do not edit src/terse.generated.ts by hand.