Skip to main content
Terse workflows combine two sources:
  • the public terse-sdk package
  • the generated helpers in src/terse.generated.ts
After terse generate, integration triggers are grouped under a top-level Triggers object (for example Triggers.github.onIssueCreated(...)). See the Triggers 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)
  • sleep, step, jobStep, log, waitForInput, slack: durable helpers for timed waits, custom steps, replay-safe logging, and human input prompts. See Durable helpers
  • Terse, TERSE_JOB_WEBHOOK_TRIGGER_PATH: client and mount path for handleTrigger when you self-host the data plane webhook endpoint
  • 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. All concrete trigger types, built-in (CronTrigger, WebhookTrigger<TBody>, WebMonitorTrigger<TStructured>, WebMonitorTriggerFor<TSchema>) and integration (GithubIssueCreatedTrigger, GithubPROpenedTrigger, SlackMessageTrigger, AttioRecordCreatedTrigger, …), 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_PROJECT_KEY

Terse and other SDK calls that reach Terse send Authorization: Bearer … from process.env.TERSE_PROJECT_KEY. Terse API tokens use the terse_ prefix and come in two flavors. Project-scoped tokens are what running workflow code uses. They are limited to runtime endpoints for one project (agent runs, tool execution, approvals, and session streams) and cannot stand in for a user token on organization or integration-management routes. Inside the Terse Cloud data plane the control plane mints one per run and removes it when the run completes; on a self-hosted data plane terse attach prints one to put in your server’s environment. User tokens are the API tokens you create in the Terse app or get from terse auth login. They live in TERSE_API_KEY and belong to the CLI, which uses them to deploy, run codegen, and manage integrations. Local runs (terse run, terse test) have no project key, so the CLI falls back to your user token and sets TERSE_PROJECT_KEY from it for the duration of the run. 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:
  • triggeredAt is a Date: 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
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.

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.
To test window logic against a fixed date, add a top-level 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.

Job state

Declare persistent state with states, 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:
  • get returns T | undefined for a key that may be unset. Give the schema a default (z.number().default(0)) and that key’s get is typed T with no undefined.
  • set returns the value it wrote, so you can use it directly instead of reading the key back.
State is stored as JSON in the project volume. It is separate from files a coding agent writes inside the sandbox. Deployed runs and 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.

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().

Filtering events

Use filter to skip runs for events that don’t match your criteria. Return true to run, false to skip.

Durable helpers

These functions are only available when durable: 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-available Triggers.schedule, Triggers.webhook, and Triggers.webMonitor.
  • Workspace resource constants generated from your workspace (for example Repos, SlackChannel, SlackUser, LinearTeam, LinearProject, NotionDatabase, PosthogProject, DatadogIndex, LaunchDarklyProject, HeyReachCampaign, and AttioObject.*).
  • PosthogEventName: a union of the custom event names observed in your PostHog project over the last 180 days.
Re-run terse generate when your integration context changes. Do not edit src/terse.generated.ts by hand.