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.

TerseAgent is the runtime that reasons and acts on your behalf: an agentic loop with tool-calling, structured outputs validated with zod, and a strict allowlist that prevents the model from touching anything you didn’t explicitly grant. Most of the time you don’t construct it yourself. generateText is the shorthand you reach for in almost every job — give it a prompt and the skills the model may use, and it runs the agentic loop to completion and returns the result. The model can call any tool granted via skills while it works.
import { generateText, createJob } from "terse-sdk"

import { AttioObject, Skills, SlackChannel, Triggers } from "./terse.generated"

createJob({
    name: "score-new-deal",
    triggers: [Triggers.attio.onRecordCreated({ object: AttioObject.Deal })],
    onTrigger: async event => {
        const summary = await generateText({
            prompt: `Score this account for ICP fit and explain why. ${event.formatForAgentRunner()}`,
            skills: [Skills.attio({ object: AttioObject.Deal }), Skills.web(), Skills.slack({ channel: SlackChannel.DealDesk })]
        })
    }
})
generateText is a thin wrapper over TerseAgent. You only construct the agent directly — TerseAgent.create() — for the two things generateText doesn’t expose: streaming partial output with run(), or reusing one agent instance across several calls. The rest of this page describes that underlying agent and the allowlist that governs both paths. For fixed, deterministic actions, skip the agent entirely and call toolbox.*.

Agentic loop

Every run executes a full agentic loop: the model picks a tool, the SDK executes it, the result is fed back, and the loop continues until the model produces a final answer. Every step is recorded in Activity so you can replay the conversation later.
CallReturnsUse when
generateText({ prompt })Final stringThe default — you want the final text once the loop completes
generateText({ prompt, outputSchema })Validated typed objectYou want structured, schema-validated JSON (see below)
agent.run(message)Async iterable of eventsYou want to stream progress (text deltas, tool calls)
agent.runAndWait(message, schema?)String or typed objectYou’re already holding a TerseAgent instance and want to reuse it
The model only sees the tools you grant through skills. Everything else is invisible to it: your environment variables, your other integrations, and your other workflows.

Structured outputs

Pass an outputSchema (a zod schema) to generateText and the SDK forwards it to the model, parses the final JSON, and validates it before returning. The result is fully typed. (On a TerseAgent, the same schema goes as the second argument to runAndWait.)
import { generateText } from "terse-sdk"
import { z } from "zod"
import { toolbox } from "./terse.generated"

const ScoreSchema = z.object({
    score: z.number().min(0).max(100),
    rationale: z.string(),
    nextAction: z.enum(["accept", "review", "reject"])
})

const scored = await generateText({
    prompt: `Score this account and explain why. ${event.formatForAgentRunner()}`,
    skills: [Skills.attio({ object: AttioObject.Deal })],
    outputSchema: ScoreSchema
})

// scored is typed as: { score: number; rationale: string; nextAction: "accept" | "review" | "reject" }
if (scored.nextAction === "accept") {
    await toolbox.attio.upsertRecord({
        object: AttioObject.Deal,
        matchingAttribute: "record_id",
        records: [{ record_id: event.record.id, fit_score: scored.score, scoring_notes: scored.rationale }]
    })
}
If the model returns malformed JSON or fails schema validation, the run errors out, Terse marks it Failed in Activity, and no half-applied side effects reach your CRM.

Strict ACL on skills and resources

The most important property of TerseAgent is what it won’t do. The Terse runtime enforces a strict allowlist on every model-driven tool call:
  • The model can only see the integrations declared in skills. If Slack isn’t in skills, the model has no way to discover or call any Slack tool, even if your workspace has Slack connected.
  • Skill configuration further narrows the surface. Skills.attio({ object: AttioObject.Deal }) means the model can read and write records on the Deal object only, not your other Attio objects or workspaces.
  • Skills.slack({ channel: SlackChannel.DealDesk }) pins messaging to one channel. The model can’t @here your #general.
  • Skills.github({ repos: [...] }) restricts code access to specific repositories.
  • Tools listed in toolApprovals are paused for human approval before execution.
Tool calls outside this allowlist are rejected before any external API is touched. skills and toolApprovals work the same whether you pass them to generateText or TerseAgent.create(). For unfiltered deterministic access from code, use the toolbox export from ./terse.generated.
const scored = await generateText({
    prompt: `Enrich and route this inbound deal. ${event.formatForAgentRunner()}`,
    skills: [
        Skills.attio({ object: AttioObject.Deal }), // Deal object only
        Skills.web(),
        Skills.slack({ channel: SlackChannel.DealDesk }) // DealDesk only
    ],
    toolApprovals: ["attio_upsert_record"] // human-in-the-loop on writes
})
If you want the model to reach more, you have to grant more, explicitly and in code, where it shows up in code review and in the generated audit trail.

Where to go next

Skills

How skills controls what the model can reach.

Deterministic tool calls

Call integration tools directly from code without the LLM.

Human-in-the-loop

Require approval before specific tools execute.

TypeScript SDK reference

Full reference for TerseAgent.create, run, and runAndWait.