Call integration tools directly from your workflow code with guaranteed execution, no LLM in the loop.
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.
The generated SDK attaches typed wrappers under agent.tools.* for every integration declared in the agent’s skills. Use them when you want a deterministic call inside an agentic handler.
import { TerseAgent, createJob } from "terse-sdk"import { AttioObject, Skills, SlackChannel, Triggers } from "./terse.generated"createJob({ name: "new-deal-alert", triggers: [Triggers.attio.onRecordCreated({ object: AttioObject.Deal })], onTrigger: async (event) => { const agent = TerseAgent.create({ prompt: "You alert the deal desk about new deals with quick context.", skills: [Skills.attio({ object: AttioObject.Deal }), Skills.slack({ channel: SlackChannel.DealDesk })] }) // Deterministic: post to Slack directly, no LLM in the loop await agent.tools.slack.sendMessage({ channelId: SlackChannel.DealDesk.channelId, message: `New deal: ${event.record.values.company_name}` }) }})
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. Note: agent.tools.* is scoped to integrations declared in the agent’s skills. If you need unfiltered access from code, use toolbox instead.
Code generation exports a toolbox object alongside agent.tools.*. It exposes the same typed integration methods, but you do not need a TerseAgent, and it is not filtered by skills. Use it when you only want deterministic tool calls.
import { toolbox, SlackChannel } from "./terse.generated"await toolbox.slack.sendMessage({ channelId: SlackChannel.DealDesk.channelId, message: "Shipped without constructing TerseAgent."})
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.
import { generateText } from "terse-sdk"import { z } from "zod"import { toolbox } from "./terse.generated"createJob({ name: "deal-enrichment-and-scoring", triggers: [Triggers.attio.onRecordCreated({ object: AttioObject.Deal })], onTrigger: async (event) => { // 1. Agentic: let the model research the company and reason about fit const parsed = await generateText({ prompt: [ "Research this company and score it for ICP fit (1-100).", `Company: ${event.record.values.company_name}`, `Domain: ${event.record.values.company_domain}` ].join("\n"), skills: [Skills.attio({ object: AttioObject.Deal }), Skills.web()], outputSchema: z.object({ score: z.number(), rationale: z.string() }) }) // 2. Deterministic: write the result back to the CRM await toolbox.attio.upsertRecord({ object: AttioObject.Deal, matchingAttribute: "record_id", records: [{ record_id: event.record.id, fit_score: parsed.score, scoring_notes: parsed.rationale }] }) // 3. Deterministic: notify the team await toolbox.slack.sendMessage({ channelId: SlackChannel.DealDesk.channelId, message: `Scored ${event.record.values.company_name}: ${parsed.score}/100 - ${parsed.rationale}` }) }})
Steps 2 and 3 always execute the same way. Step 1 is where the model adds value: researching and reasoning about data that would be hard to codify as rules.