- the public
terse-sdkpackage - the generated helpers in
src/terse.generated.ts
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 fromterse-sdk:
createJob,CreateJobParameters: register a workflow (call at module top level or from imported modules)sleep,step,jobStep,log,waitForInput,slack: durable helpers for timed waits, custom steps, replay-safe logging, and human input prompts. See Durable helpersgenerateText: the shorthand for agentic runs — one prompt in, final output out, with the model free to call any tool granted viaskills. Use this for almost everything; seegenerateTextbelowTerse,TERSE_JOB_WEBHOOK_TRIGGER_PATH: client and mount path forhandleTriggerwhen you self-host the data plane webhook endpointTerseAgent,EventType: the lower-level agent thatgenerateTextwraps. Reach forTerseAgent.create()directly only when you need to stream withrun()or reuse one agent instance across calls;EventTypeenumerates values on streamed resultsrunWithJobContext,getJobContext,TerseJobContext: wrap a handler withrunWithJobContextto preserve run attribution. Outside of it, setTERSE_BACKEND_URLif needed (defaulthttps://api.useterse.ai) and optionallyTERSE_RUN_IDformatTriggerForAgent,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.
All concrete trigger types, built-in (
CronTrigger, WebhookTrigger<TBody>, WebMonitorTrigger<TStructured>, WebMonitorTriggerFor<TSchema>) and integration (GithubPROpenedTrigger, SlackMessageTrigger, AttioRecordCreatedTrigger, …), along with tool Params/Result types, are generated into your workspace by terse generate; import them from "./terse.generated".
For TypeScript workflows that use structured output schemas, also install and import 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 auth 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
Trigger types exported from./terse.generated extend the canonical trigger object with the trigger time plus two methods used everywhere Terse turns an event into prompts or logging:
triggeredAtis aDate: the moment Terse received the trigger. See trigger time.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
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.
Trigger time
event.triggeredAt is stamped once when the trigger arrives, so it reads the same after queue delay, retries, and durable replays. Anchor date math to it rather than Date.now() or new Date(), which drift.
triggeredAt ISO 8601 timestamp to an event fixture passed to terse test.
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. The scaffolded layout defines each job in its own file under src/jobs/ and imports it for side effects (import "./jobs/myWorkflow") from the entry file.
Duplicate name values throw an error when the second job registers. At most one webhook trigger is allowed per workflow.
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.
Job state
Declare persistent state withstates, a list of { key, value } where value is a Zod schema. filter and onTrigger receive a state object as their second argument. state.get(key) and state.set(key, value) are narrowed to the declared keys, with each value typed and validated against that key’s schema.
Two type details worth knowing:
getreturnsT | undefinedfor a key that may be unset. Give the schema a default (z.number().default(0)) and that key’sgetis typedTwith noundefined.setreturns the value it wrote, so you can use it directly instead of reading the key back.
terse test runs each have their own copy of a job’s state, so testing never touches production values. Test state persists across terse test invocations, which lets you exercise dedupe and cooldown logic locally; reset it with terse test run --fresh-state or inspect it with the terse state commands.
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.
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.
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().
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).
run when you want to stream output progressively.
runAndWait(userMessage)
Waits for the model to complete and returns the final output string.
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.
TerseAgent.executeTool(toolName, params?)
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.
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).
toolbox.* (generated)
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.
agent.tools.*
Generated helpers attach deterministic wrappers under agent.tools.*. These call integration actions directly, not through the LLM.
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
PassoutputSchema 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
Usefilter to skip runs for events that don’t match your criteria. Return true to run, false to skip.
Tool approvals
List tool names intoolApprovals 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.
toolApprovals for workflows that write to production systems during early development, or when compliance requires a human in the loop.
Durable helpers
These functions are only available whendurable: true is set on the job. Calling them in a non-durable job throws DurableOnlyError.
sleep(duration)
Suspends the run until the timer fires. Accepts ms-style strings ("30s", "5m", "1h", "3d"), a millisecond number, or a Date. Locally (terse test, no TERSE_RUN_ID), the wait is skipped and a log line notes what production would have done.
step(call)
Wrapped around a direct call to a module-scope client or function, runs that call as a durable step: step(resend.emails.send({...})). Arguments evaluate in the handler and cross the boundary as serialized data; the resolved value is journaled. See Durability — Making your own calls durable with step().
jobStep(...)
The fully explicit form of step(): declared input, zod validation at the boundary, no restrictions on the step body. See Durability — jobStep: the fully explicit form.
log(...args)
Replay-safe logging: await log("processed", count). In a durable job the call is a journaled step, so the line prints exactly once instead of once per replay like a bare console.log outside a step. Arguments cross the step boundary, so they must be serializable data. Unlike the other durable helpers, log() also works in non-durable jobs, where it simply forwards to console.log. See Durability — Logging.
waitForInput(...)
Posts an interactive input request and suspends until a human responds. Returns a typed InputResponse with choice, optional text (when the selected option has freeText: true), respondent, and delivery.
result.choice is typed to the union of your option ids. See Durability — Waiting for human input.
Locally (terse test), Terse posts the same Slack message it would in production, marked as coming from terse test, and the run waits for the response there. The wait is journaled as a step, so durable replays return the cached answer instead of asking again.
slack({ channel })
Target constructor for waitForInput. Pass a Slack channel id (for example SlackChannel.DealDesk.channelId from generated helpers).
Events
Event typing depends on the trigger.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-availableTriggers.schedule,Triggers.webhook, andTriggers.webMonitor.Skills: per-integration skill factories for every connected integration, plus the built-inSkills.web()andSkills.imageEdit().- Workspace resource constants generated from your workspace (for example
Repos,SlackChannel,SlackUser,LinearTeam,LinearProject,NotionDatabase,PosthogProject,DatadogIndex,LaunchDarklyProject,HeyReachCampaign, andAttioObject.*). PosthogEventName: a union of the custom event names observed in your PostHog project over the last 180 days, soeventNamefilters onsearchPosthogEventsare checked at compile time.toolbox: deterministic tool wrappers you can call without constructing an agent or listingskills.agent.tools.*: the same wrappers attached to aTerseAgent, filtered by theskillsyou passed.
