Skip to main content
Most AI workflows have a problem: every action runs through the model. Need to update a CRM record? The model decides if, when, and how. Need to send a Slack message? Same thing. This works when you need judgment, but it’s wasteful and unreliable when you already know exactly what to do. Deterministic tool calls let you call integration tools directly from your workflow code. No model reasoning, no token spend, no chance the LLM decides to skip the step or call the wrong tool. The backend still handles credentials and OAuth. You just skip the AI middleman.

When to use each approach

DeterministicAgentic
How it worksYour code calls a specific tool with exact parametersThe model chooses which tools to call based on a prompt
API surfaceagent.tools.* or executeTool()run() or runAndWait()
When to useYou know the tool, parameters, and order in advanceYou need the model to reason, summarize, or decide
PredictabilityAlways runs the same wayOutput varies across runs
Token costZero LLM tokensTokens consumed per run

agent.tools.*

The generated SDK attaches typed wrappers under agent.tools.* for every connected integration. These are the primary way to make deterministic calls.
import { Terse, TerseAgent, Trigger } from "terse-sdk"

import { Apollo, Attio, AttioList, Slack, SlackChannel } from "./terse.generated"

const client = new Terse()

await client.createJob({
    name: "new-deal-alert",
    triggers: [Attio.onRecordCreated({ list: AttioList.Pipeline.NewDeals })],
    skills: [Attio.skill({ lists: [AttioList.Pipeline.NewDeals] }), Apollo.skill(), Slack.skill({ channel: SlackChannel.DealDesk })],
    onTrigger: async (event: Trigger, agent: TerseAgent) => {
        // Deterministic: enrich the company directly
        const enrichment = await agent.tools.apollo.enrichCompany({
            domain: event.record.values.company_domain
        })

        // Deterministic: post to Slack directly
        await agent.tools.slack.sendMessage({
            channelId: SlackChannel.DealDesk.channelId,
            message: [`New deal: ${event.record.values.company_name}`, `Industry: ${enrichment.industry}`, `Employees: ${enrichment.employeeCount}`].join("\n")
        })
    }
})
Every method on agent.tools.* is generated by terse generate based on your connected integrations. The wrappers are fully typed, so your editor autocompletes tool names, parameter shapes, and return types.

executeTool

For lower-level control, call any tool by its string name. This is useful when the tool name is dynamic or when a generated wrapper doesn’t exist yet.
const result = await agent.executeTool("attio.updateRecord", {
    list: AttioList.Pipeline.NewDeals,
    recordId: event.record.id,
    fields: { fit_score: 92 }
})
executeTool hits the same backend path as agent.tools.*. The only difference is that you pass the tool name as a string instead of calling a typed wrapper.

Mixing deterministic and agentic calls

The real power is combining both in a single workflow. Use deterministic calls for predictable operations and hand off to the model when you need reasoning.
await client.createJob({
    name: "deal-enrichment-and-scoring",
    triggers: [Attio.onRecordCreated({ list: AttioList.Pipeline.NewDeals })],
    skills: [Attio.skill({ lists: [AttioList.Pipeline.NewDeals] }), Apollo.skill(), Slack.skill({ channel: SlackChannel.DealDesk })],
    onTrigger: async (event: Trigger, agent: TerseAgent) => {
        // 1. Deterministic: fetch enrichment data
        const enrichment = await agent.tools.apollo.enrichCompany({
            domain: event.record.values.company_domain
        })

        // 2. Agentic: let the model reason about fit
        const analysis = await agent.runAndWait(
            [
                "Score this company for ICP fit (1-100) and explain your reasoning.",
                `Company: ${event.record.values.company_name}`,
                `Industry: ${enrichment.industry}`,
                `Employees: ${enrichment.employeeCount}`,
                `Technologies: ${enrichment.technologies.join(", ")}`,
                'Respond with JSON: { "score": <number>, "rationale": "<string>" }'
            ].join("\n"),
            event
        )

        const parsed = JSON.parse(analysis) as { score: number; rationale: string }

        // 3. Deterministic: write the result back to the CRM
        await agent.tools.attio.updateRecord({
            list: AttioList.Pipeline.NewDeals,
            recordId: event.record.id,
            fields: {
                fit_score: parsed.score,
                scoring_notes: parsed.rationale
            }
        })

        // 4. Deterministic: notify the team
        await agent.tools.slack.sendMessage({
            channelId: SlackChannel.DealDesk.channelId,
            message: `Scored ${event.record.values.company_name}: ${parsed.score}/100 - ${parsed.rationale}`
        })
    }
})
Steps 1, 3, and 4 always execute the same way. Step 2 is where the model adds value: reasoning about data that would be hard to codify as rules.

How it works under the hood

When you call agent.tools.* or executeTool, the SDK sends a POST request to the Terse backend with the tool name and parameters. The backend resolves the tool against your connected integrations, uses your stored OAuth tokens and credentials, executes the tool, and returns the result. No model is involved at any point.
Your code → SDK → POST /sdk/tool-execute → Backend resolves tool → OAuth/credentials → Integration API → Result
The same tools are available to the model during run and runAndWait. The difference is only in who decides to call them.

Available deterministic tools

Every integration you connect generates deterministic wrappers. Run terse generate to see what’s available in your project. Common examples:
IntegrationExample tools
AttioupdateRecord, getRecord, createRecord, listRecords
ApolloenrichCompany, enrichPerson
SlacksendMessage, updateMessage
SnowflakeexecuteQuery
GitHubcreateIssue, addComment, listPullRequests
Httprequest (call any REST API)
Re-run terse generate when your connected integrations change. Do not hand-edit src/terse.generated.ts or src/terse_generated.py.

Where to go next

Context as Code

How terse generate creates typed helpers from your workspace.

TypeScript SDK reference

Full reference for executeTool, agent.tools.*, and more.