Skip to main content
Context as Code is the idea. The generated SDK is the artifact. Running terse generate writes a single typed file to your project:
  • src/terse.generated.ts for TypeScript
Every trigger, skill, deterministic tool wrapper, and real workspace resource (channels, repos, lists, objects, owners) is exported as a typed value. Your workflows import from it. Your coding agent reads it. The compiler enforces it.
The generated file is deterministic, overwritten on every terse generate, and committed alongside your workflows. Do not hand-edit it.

What the file contains

Each section is generated from a real integration you’ve connected. Here’s a trimmed snapshot of what you’d see after connecting Slack, GitHub, and Attio.

Resource constants

Every workspace resource is exported as a typed constant. No UUIDs in your workflow code, no runtime lookups.
src/terse.generated.ts
export class SlackChannel {
    constructor(
        public readonly channelId: string,
        public readonly name: string
    ) {}

    static DealDesk = new SlackChannel("C08XYZ1234", "deal-desk")
    static Engineering = new SlackChannel("C09ABC5678", "engineering")
    static AllTerseInc = new SlackChannel("C01DEF9012", "all-terse-inc")
}

export class GithubOwner {
    constructor(public readonly name: string) {}

    static TerseAI = new GithubOwner("terse-ai")
}

export class Repos {
    constructor(
        public readonly repositoryId: number,
        public readonly name: string,
        public readonly owner: GithubOwner,
        public readonly fullName: string
    ) {}

    static TerseAI = {
        Terse: new Repos(829471023, "terse", GithubOwner.TerseAI, "terse-ai/terse"),
        Docs: new Repos(829471108, "docs", GithubOwner.TerseAI, "terse-ai/docs")
    } as const
}

export class AttioList {
    constructor(
        public readonly listId: string,
        public readonly name: string
    ) {}

    static Pipeline = {
        NewDeals: new AttioList("f1e3c0b2-…", "new-deals"),
        OpenDeals: new AttioList("8d22a17c-…", "open-deals"),
        ClosedWon: new AttioList("4b90f54e-…", "closed-won")
    } as const
}

Trigger builders and skills

Each integration becomes a namespace of typed trigger builders and a skill() constructor. The parameters take resource constants, not strings.
src/terse.generated.ts
export const Slack = {
    /** Trigger on any message in a channel */
    onMessage(opts: { channel: SlackChannel; userIds?: string[] }): TypedTrigger<SlackMessageTrigger> {
        /* … */
    },
    /** Trigger when the Slack app is directly mentioned */
    onAppMention(opts: { channel: SlackChannel }): TypedTrigger<SlackAppMentionTrigger> {
        /* … */
    },
    /** Use in `skills[]` */
    skill(opts: { channel: SlackChannel }): TypedSkill<SlackTools> {
        /* … */
    }
}

export const GitHub = {
    /** Trigger when a pull request is opened */
    onPROpened(opts: { repo: Repos }): TypedTrigger<GithubPROpenedTrigger> {
        /* … */
    },
    /** Trigger when a pull request is merged */
    onPRMerged(opts: { repo: Repos }): TypedTrigger<GithubPRMergedTrigger> {
        /* … */
    },
    /** Use in `skills[]` */
    skill(opts: { repos: Repos[] }): TypedSkill<GitHubTools> {
        /* … */
    }
}

Deterministic tool wrappers

Every connected integration also generates typed wrappers on agent.tools.*, with the exact parameter and return types of each tool. These are the calls you make when you know what to do and don’t want to burn tokens letting a model decide.
src/terse.generated.ts
export type GeneratedTools = {
    slack: {
        /** Send a message to a Slack channel */
        sendMessage(args: { channelId: string; message: string }): Promise<{ message_ts: string; channel_id: string }>
        /** Update an existing message in place */
        updateMessage(args: { channelId: string; ts: string; message: string }): Promise<SlackMessageResult>
    }
    attio: {
        /** Update fields on an Attio record */
        updateRecord(args: { list: AttioList; recordId: string; fields: Record<string, unknown> }): Promise<AttioRecord>
        /** Query records in a list */
        queryRecords(args: { list: AttioList; filter?: AttioFilter }): Promise<AttioRecord[]>
    }
    github: {
        /** Open a comment on a pull request */
        addComment(args: { repo: Repos; pullNumber: number; body: string }): Promise<GithubComment>
    }
}

declare module "terse-sdk" {
    interface TerseAgent {
        readonly tools: GeneratedTools
    }
}

What you write against it

Because every resource, trigger, skill, and tool is an export, your workflow code reads like a description of your business, not a scavenger hunt through admin UIs.
src/index.ts
import { Terse, TerseAgent, Trigger } from "terse-sdk"

import { AttioList, GitHub, Repos, Slack, SlackChannel } from "./terse.generated"

const client = new Terse()

await client.createJob({
    name: "pr-summary-to-slack",
    triggers: [GitHub.onPROpened({ repo: Repos.TerseAI.Terse })],
    skills: [GitHub.skill({ repos: [Repos.TerseAI.Terse] }), Slack.skill({ channel: SlackChannel.Engineering })],
    onTrigger: async (event: Trigger, agent: TerseAgent) => {
        const message = await agent.tools.slack.sendMessage({
            channelId: SlackChannel.Engineering.channelId,
            message: `New PR from ${event.sender.login}: ${event.pull_request.title}`
        })

        await agent.runAndWait(`Summarize this PR and reply in thread (ts: ${message.message_ts}).`, event)
    }
})
No channel IDs. No repository numbers. No list UUIDs. The only strings you write are your own content.

Why humans love it

Before the generated SDK, shipping a workflow meant context switching between your editor, your CRM admin, your Slack workspace, and GitHub settings to collect IDs.
  • No more ID hunting. SlackChannel.DealDesk replaces "C08XYZ1234". Repos.TerseAI.Terse replaces a numeric repository ID you’d otherwise have to dig out of a URL.
  • Autocomplete instead of documentation. Your editor lists every connected channel, list, and repo inline. Hover for its real name and ID.
  • Renames are tracked. When #deal-desk gets renamed in Slack, the next terse generate rewrites the constant. Your code keeps working against SlackChannel.DealDesk.
  • Reviews read like English. PRs show AttioList.Pipeline.NewDeals, not "f1e3c0b2-…". Teammates can actually tell what changed.

Why coding agents love it even more

The generated SDK is the difference between an agent that guesses and an agent that knows. This is where the compounding wins live.
  • The workspace is the prompt. Cursor, Claude Code, Windsurf, and Codex see every real channel, repo, and list as an exported symbol. They don’t need to ask you “which channel should this post to?” — they pick the one that exists.
  • No copy-pasted UUIDs. Agents are notoriously bad at preserving long opaque strings across tool calls. With resource constants, there’s nothing to copy.
  • Hallucinations stop compiling. If an agent invents SlackChannel.SalesTeam or Repos.TerseAI.Website, the TypeScript compiler rejects the build. terse deploy refuses to ship code that doesn’t type-check. There is no path to production for a made-up resource.
  • Tool parameters are typed end-to-end. agent.tools.slack.sendMessage takes { channelId: string; message: string }. An agent can’t accidentally pass channel_name or forget message — the signature is enforced.
  • The agent can read the same file you do. Point your coding assistant at src/terse.generated.ts and it instantly has a complete map of the integrations, resources, and tools available in your workspace. No documentation gap.
The net effect: coding agents ship working workflows on the first try, without a human looping through “here is the channel ID, here is the list ID, here is the repo name” every time.

How it stays in sync

terse generate is idempotent and safe to re-run. Typical triggers for regenerating:
1

You connect a new integration

Run terse integrate or connect from the Terse app, then terse generate to pick up new trigger builders, skills, and tool wrappers.
2

Workspace resources change

A new Slack channel, a new Attio list, a renamed repo. Re-run terse generate and commit the updated file.
3

Before every deploy

The CLI re-runs generation as part of terse deploy, so you never ship against stale context.
Because the file is committed to your repo, git history gives you a full audit trail of how your workspace has changed over time.

Where to go next

Context as Code

The philosophy behind compiling your workspace into typed code.

Skills & integrations

How the generated skills and triggers fit into a workflow.

Deterministic tool calls

Use agent.tools.* to call integrations directly, no LLM in the loop.

TypeScript SDK reference

Full reference for the runtime types the generated file imports from.