Skip to main content
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.

Integrations: stored credentials

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:
TypeFlowExamples
OAuthOpens your browser for authorizationGitHub, Slack, Gmail, Linear, Notion, WorkOS
FormPrompts for API keys or account credentials in the terminalDatadog, PostHog, Snowflake, LaunchDarkly, Attio
After connecting, run terse generate to turn your active integrations into typed code.

Code generation: from credentials to code

terse generate reads your connected integrations and their resources (repos, channels, lists, teams, projects), then writes a typed SDK file for your project:
  • src/terse.generated.ts for TypeScript
The generated file gives you four things:
ExportWhat it doesExample
Trigger buildersStart a workflow from an integration eventSlack.onMessage(), Attio.onRecordCreated()
Skill constructorsDeclare what the model can useSlack.skill(), Apollo.skill()
Resource constantsReference real workspace objects by nameSlackChannel.DealDesk, AttioList.Pipeline.NewDeals
Deterministic tool wrappersCall tools directly from codeagent.tools.slack.sendMessage()
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.

Skills: controlling model access

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.

Skills vs. deterministic tools

This is the key distinction:
Skills (model access)Deterministic tools (code access)
Who decidesThe model chooses which tools to callYour code calls tools directly
Controlled byskills array on the jobAlways available via agent.tools.*
ScopeOnly integrations declared as skillsAny connected integration
Token costTokens consumed per callZero LLM tokens
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
        })
    }
})

Skill configuration

Some skills accept configuration that scopes what the model sees or where it acts:
SkillConfigurationEffect
Attio.skill()listsLimits which CRM lists the model can query
Slack.skill()channelTargets a specific channel for messaging
GitHub.skill()reposScopes code access to specific repositories
Snowflake.skill()NoneRead-only SQL queries (approval required by default)
Skills without configuration give the model access to the full integration surface.

Where to go next

Skills & tools reference

Full list of every skill and the tools it exposes.

Deterministic tool calls

Call tools directly from code without the LLM.