Connect external services, generate typed code, and control what the model can reach.
Integrations and skills are two sides of the same coin. An integration is a connected external service: your CRM, enrichment provider, messaging tool, or data warehouse. A skill is the declaration that makes that integration available to your workflow. You connect once, generate typed code, and then choose per-job which integrations the model can use.
An integration is a connection to an external service. When you run terse integrate or connect through the Terse app, the platform stores your OAuth tokens or API keys securely. The integration itself is just an authenticated link to a service.Terse supports two connection types:
Type
Flow
Examples
OAuth
Opens your browser for authorization
GitHub, Slack, Gmail, Linear, Notion, WorkOS
Form
Prompts for API keys or account credentials in the terminal
Datadog, PostHog, Snowflake, LaunchDarkly, Attio
After connecting, run terse generate to turn your active integrations into typed code.
terse generate reads your connected integrations and their resources (repos, channels, lists, teams, projects), then writes a typed SDK file for your project:
Everything is typed against the actual resources in your workspace. Your editor autocompletes channel names, list names, and tool parameters, and your coding agent (Cursor, Claude Code, etc.) builds correct workflows without you looking up IDs.
Re-run terse generate when your connected integrations change. Do not hand-edit src/terse.generated.ts or src/terse_generated.py.
A skill tells Terse which integrations the model is allowed to use during agentic calls (run and runAndWait). When the backend receives an agentic request, it looks at the declared skills, maps each one to an integration type, and filters the available tools to only those integrations. The model never sees tools from integrations you didn’t declare as skills.
await client.createJob({ name: "deal-enrichment", 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) => { // The model can use Attio, Apollo, and Slack tools const summary = await agent.runAndWait("Enrich and summarize this deal.", event) }})
In this example, the model can read Attio records, call Apollo for enrichment, and post to Slack because all three are declared as skills. If you removed Apollo.skill() from the list, the model would lose access to Apollo tools during runAndWait, even though Apollo is still connected as an integration.
Deterministic tool wrappers (agent.tools.*) are not gated by the skills list. If you have Slack connected and generated, you can always call agent.tools.slack.sendMessage() from your handler code, even if Slack isn’t in your skills array. The skills array only controls what the model sees when it’s reasoning.This means you can be precise about the model’s reach while keeping full programmatic access to all your integrations:
await client.createJob({ name: "weekly-digest", triggers: [Schedule.cron("0 9 * * 1")], skills: [Attio.skill({ lists: [AttioList.Pipeline.NewDeals] })], onTrigger: async (event: Trigger, agent: TerseAgent) => { // Agentic: model can only use Attio (the declared skill) const summary = await agent.runAndWait("Summarize this week's pipeline activity.", event) // Deterministic: code can always call Slack directly await agent.tools.slack.sendMessage({ channelId: SlackChannel.DealDesk.channelId, message: summary }) }})