Skip to main content

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 trigger builders and skill factories are grouped under top-level Triggers and Skills objects (for example Triggers.github.onPROpened(...), Triggers.heyReach.onMessageReplyReceived(...), Skills.slack({ channel: ... })). System-style helpers live there too: Triggers.schedule.cron(...), Triggers.webhook.onRequest(), and Triggers.webMonitor.onEvent(...). Prefer these namespaces instead of older per-integration root exports such as GitHub.*, Slack.*, or Schedule.* / Webhook.*.
Hosted workflow runs use the Node.js runtime (terse-sdk and your generated src/terse.generated.ts). Terse does not ship a Python runtime or Python workflow SDK.

Core SDK imports

Import these from terse-sdk:
  • createJob: registers a workflow (call at module top level or from imported modules)
  • CreateJobParameters: the object shape passed to createJob
  • Terse: client for handleTrigger when you self-host the webhook endpoint
  • TERSE_JOB_WEBHOOK_TRIGGER_PATH: mount this path on your HTTP server when wiring handleTrigger (matches what the Terse backend calls)
  • TerseAgent: create with TerseAgent.create() inside onTrigger
  • EventType: enum for values on streamed TerseAgent results
  • Trigger: the base event interface
  • Specific event types: GithubTrigger, GithubPRTrigger, CronTrigger, ManualSampleTrigger, WebhookTrigger (optionally generic over the JSON body type), HeyReach types such as HeyReachMessageReplyReceivedTrigger and HeyReachTrigger, and other per-integration exports (see terse-sdk package entry)
  • Config helpers re-exported for trigger construction: HeyReachInputConfig, and the HeyReachEventType object for event-type constants
  • Run history types (re-exported for Activity and API payloads): RunHistoryRecord, RunHistoryStatus, RunHistoryTrigger, RunHistoryDecision, RunHistoryAction
  • formatTriggerForAgent and debugTrigger: helpers re-exported from terse-types for plain trigger payloads (without the SDK methods below)
  • getJobContext, runWithJobContext, type TerseJobContext: async job context (session id, run id, API base URL). The SDK reads this when each request is made so agent runs and deterministic tool calls stay attributed to the right session and run. Outside runWithJobContext, set TERSE_BACKEND_URL if needed (default https://app.useterse.ai/api) and optionally TERSE_RUN_ID for run attribution.
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.
  • 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 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 hosted Modal sandboxes. They are limited to SDK runtime endpoints for that project (for example agent runs, tool execution, approvals, and session streams) and cannot stand in for a user token on organization or integration-management routes. Terse 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 are the canonical trigger object plus 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.
FieldTypeDescription
namestringUnique workflow identifier. Used to match workflows across deploys and in terse test.
triggerstyped trigger arrayOne 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.
remoteServerUrlstringOptional override for the Terse API base URL (otherwise TERSE_BACKEND_URL or https://app.useterse.ai/api).
Pass skills, toolApprovals, and the agent prompt to TerseAgent.create() inside onTrigger, not on createJob.
import { GithubPRTrigger, createJob, TerseAgent } 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
    }
})

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, TERSE_JOB_WEBHOOK_TRIGGER_PATH } from "terse-sdk"

const terse = new Terse()

// Mount TERSE_JOB_WEBHOOK_TRIGGER_PATH on your HTTP server (Express, Hono, Next.js, etc.)

TerseAgent

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 }

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.
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 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.
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() (or when the integration is implied by a trigger). For the same calls without listing skills, use generated toolbox instead.

Structured output

Use runAndWait(message, schema) when you need typed, validated JSON. Use runAndWait(message) 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}.`
        })
    }
})

Tool approvals

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.sendMessage"]
        })

        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 sourceEvent type
Attio record eventsTrigger
Slack actionsTyped Slack action payloads
Scheduled cron triggersCronTrigger
Scheduled interval triggersCronTrigger
Manual triggerManualSampleTrigger
Webhook HTTP requestsWebhookTrigger
HeyReach outreach eventsHeyReachTrigger (union) or the specific HeyReach*Trigger for your handler
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 (when connected), including Triggers.heyReach when HeyReach is connected, plus Triggers.schedule, Triggers.webhook, and Triggers.webMonitor
  • Skills: per-integration skill factories (when connected) plus built-in Skills.web() and Skills.imageEdit()
  • Workspace resource constants (for example Repos, SlackChannel, SlackUser, AttioObject.* statics) and other typed prelude exports your workspace needs
  • toolbox: deterministic tool wrappers without an agent or skills filter
  • Deterministic tool wrappers on TerseAgent via agent.tools.* (filtered by skills)
Re-run terse generate when your integration context changes. Do not edit src/terse.generated.ts by hand.

Bring your own API

Call REST endpoints from onTrigger with fetch (or your HTTP client of choice), then pass results into TerseAgent.runAndWait or deterministic agent.tools.* / TerseAgent.executeTool calls. Pair that with Triggers.webhook.onRequest when an external system should start the workflow, and use Skills.web() when you want the built-in web research tools on the agent.