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.
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 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.
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.
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.