> ## Documentation Index
> Fetch the complete documentation index at: https://docs.useterse.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Skills & Tools

> All available skills and the tools they expose to your workflows.

Skills define what integrations your workflow can interact with. Each skill exposes a set of tools that the agent can call (via agentic loops) or that you can call deterministically (via `agent.tools.*`).

In TypeScript, `terse generate` writes a single `Skills` object in `src/terse.generated.ts`. Import it and call the nested factory for the integration you want (for example `Skills.github({ repos: [...] })`). Built-in capabilities like `Skills.web()`, `Skills.imageEdit()`, and `Skills.memory()` are documented in their sections below.

## GitHub

Read-only access to repositories.

```ts theme={null}
skills: [Skills.github({ repos: [Repos.MyOrg.MyRepo] })]
```

| Tool                             | Description                              | Access |
| -------------------------------- | ---------------------------------------- | ------ |
| `searchGitHubCode`               | Semantic code search across repositories | Read   |
| `grepGitHubCode`                 | Exact-match code search (grep-like)      | Read   |
| `readGitHubFile`                 | Read file contents                       | Read   |
| `listGitHubDirectory`            | List directory contents                  | Read   |
| `listGitHubPullRequests`         | List pull requests                       | Read   |
| `listGitHubCommits`              | List commits                             | Read   |
| `summarizeGitHubPullRequestDiff` | Summarize PR changes                     | Read   |

## Slack

Send messages and read conversations.

```ts theme={null}
skills: [Skills.slack({ channel: SlackChannel.Engineering })]
```

| Tool                      | Description                                                                      | Access |
| ------------------------- | -------------------------------------------------------------------------------- | ------ |
| `slack_send_message`      | Send to a channel, an existing DM (`channelId`), or a member 1:1 (`slackUserId`) | Write  |
| `slack_list_users`        | List workspace users                                                             | Read   |
| `slack_list_channels`     | List channels                                                                    | Read   |
| `slack_read_conversation` | Read conversation history                                                        | Read   |

For `slack_send_message`, pass either `channelId` (a public or private channel id, or an existing DM channel id) or `slackUserId` (a member id, `U…`). Terse opens the DM if needed. If you pass both, `channelId` wins and the message goes there. After `terse generate`, use `SlackChannel.*.channelId` and `SlackUser.*.userId` from `src/terse.generated.ts` instead of hand-copying ids.

## Linear

Create, update, and search issues.

```ts theme={null}
skills: [Skills.linear()]
```

| Tool                   | Description               | Access |
| ---------------------- | ------------------------- | ------ |
| `linear_search_ticket` | Search tickets            | Read   |
| `linear_get_teams`     | List teams                | Read   |
| `linear_get_states`    | Get issue states          | Read   |
| `linear_get_labels`    | Get available labels      | Read   |
| `linear_get_projects`  | List projects             | Read   |
| `linear_get_users`     | List team members         | Read   |
| `linear_read_ticket`   | Read ticket details       | Read   |
| `linear_create_ticket` | Create a new ticket       | Write  |
| `linear_update_ticket` | Update a ticket           | Write  |
| `linear_add_comment`   | Add a comment to a ticket | Write  |

## Attio

Query and manage CRM records.

```ts theme={null}
skills: [Skills.attio({ object: AttioObject.People })]
```

| Tool                      | Description                                                                              | Access |
| ------------------------- | ---------------------------------------------------------------------------------------- | ------ |
| `attio_read_records`      | Read records: `query`, `search`, `get`, `get_attribute_history`                          | Read   |
| `attio_create_record`     | Create a record (no matching attribute needed)                                           | Write  |
| `attio_update_record`     | Update a record by ID                                                                    | Write  |
| `attio_upsert_record`     | Create-or-update one or more records matched on a unique attribute                       | Write  |
| `attio_delete_record`     | Permanently delete a record                                                              | Write  |
| `attio_workspace_members` | Look up workspace members (`list`, `get`) to resolve record owners                       | Read   |
| `attio_read_tasks`        | Read tasks: `list`, `get`                                                                | Read   |
| `attio_create_task`       | Create a task                                                                            | Write  |
| `attio_update_task`       | Update a task's deadline, completion, assignees or linked records                        | Write  |
| `attio_delete_task`       | Permanently delete a task                                                                | Write  |
| `attio_read_notes`        | Read notes: `list`, `get`                                                                | Read   |
| `attio_create_note`       | Create a note on a record                                                                | Write  |
| `attio_delete_note`       | Permanently delete a note                                                                | Write  |
| `attio_read_comments`     | Read comments and threads: `get`, `list_threads`, `get_thread`                           | Read   |
| `attio_create_comment`    | Create a comment (reply to a thread or start one on a record)                            | Write  |
| `attio_delete_comment`    | Permanently delete a comment                                                             | Write  |
| `attio_read_lists`        | Read lists: `list`, `get`                                                                | Read   |
| `attio_create_list`       | Create a list over an object                                                             | Write  |
| `attio_update_list`       | Rename a list                                                                            | Write  |
| `attio_read_list_entries` | Read list entries: `query_entries`, `get_entry`                                          | Read   |
| `attio_add_list_entry`    | Add a record to a list                                                                   | Write  |
| `attio_upsert_list_entry` | Add-or-update a list entry keyed by its parent record                                    | Write  |
| `attio_update_list_entry` | Update a list entry's attribute values (e.g. move stage)                                 | Write  |
| `attio_remove_list_entry` | Remove an entry from a list (record untouched)                                           | Write  |
| `attio_meetings`          | Meetings, call recordings and transcripts                                                | Read   |
| `attio_read_files`        | Read files on records: `list`, `get`, `get_download_url`                                 | Read   |
| `attio_upload_file`       | Upload a file to a record                                                                | Write  |
| `attio_delete_file`       | Permanently delete a file                                                                | Write  |
| `attio_read_schema`       | Read workspace schema: `list_objects`, `get_object`, attributes/statuses/select options  | Read   |
| `attio_modify_schema`     | Change the workspace schema: create/update objects, attributes, statuses, select options | Write  |

## Notion

Read and write databases and pages. Optionally scope to specific databases or pages.

```ts theme={null}
import { NotionDatabase, NotionPage, Skills } from "./terse.generated"

skills: [Skills.notion({ databases: [NotionDatabase.Roadmap], pages: [NotionPage.OnCallRunbook] })]
```

`databases` and `pages` are both optional. Omit them to give the model access to every database and page the integration can reach.

| Tool                                   | Description                       | Access |
| -------------------------------------- | --------------------------------- | ------ |
| `notion_get_schema`                    | Get database schema               | Read   |
| `notion_query_database`                | Query a database                  | Read   |
| `notion_query_page`                    | Query page content                | Read   |
| `notion_list_users`                    | List workspace users              | Read   |
| `notion_create_or_update_database_row` | Create or update database rows    | Write  |
| `notion_create_or_update_page`         | Create or update standalone pages | Write  |
| `notion_modify_blocks`                 | Modify page blocks                | Write  |

## Gmail

Send emails (auto-send, no human review).

```ts theme={null}
skills: [Skills.gmail()]
```

| Tool               | Description                     | Access |
| ------------------ | ------------------------------- | ------ |
| `gmail_send_email` | Send emails or reply to threads | Write  |

## Gmail Draft

Create draft emails for a human to review and send manually.

```ts theme={null}
skills: [Skills.gmailDraft()]
```

| Tool                 | Description                          | Access |
| -------------------- | ------------------------------------ | ------ |
| `gmail_create_draft` | Create draft emails for human review | Write  |

## Meta Ads

Read ad performance and manage ads, audiences, and offline conversions.

```ts theme={null}
import { MetaAdsAdAccount, Skills } from "./terse.generated"

skills: [Skills.metaAds({ adAccount: MetaAdsAdAccount.Primary, pageId: "1234567890" })]
```

`adAccount` and `pageId` scope the agent: a tool call naming a different ad account or Page is denied. Omit both and the agent may use anything the connected login can reach. `adAccountId` is required on every tool that takes one; use the generated `MetaAdsAdAccount` values rather than raw strings.

| Tool                             | Description                                                             | Access |
| -------------------------------- | ----------------------------------------------------------------------- | ------ |
| `meta_ads_list_ad_accounts`      | List the ad accounts the connected login can reach                      | Read   |
| `meta_ads_list_campaigns`        | List campaigns with status, objective, and budget                       | Read   |
| `meta_ads_list_adsets`           | List ad sets, optionally narrowed to one campaign                       | Read   |
| `meta_ads_list_ads`              | List ads with the creative attached to each one                         | Read   |
| `meta_ads_list_audiences`        | List custom audiences with approximate sizes                            | Read   |
| `meta_ads_list_pixels`           | List pixels (Conversions API dataset IDs) in an ad account              | Read   |
| `meta_ads_list_pages`            | List the Facebook Pages the connected user manages                      | Read   |
| `meta_ads_read_insights`         | Spend, impressions, clicks, and conversions at campaign/ad set/ad level | Read   |
| `meta_ads_add_audience_users`    | Add people to a custom audience (emails/phones hashed)                  | Write  |
| `meta_ads_remove_audience_users` | Remove people from a custom audience (emails/phones hashed)             | Write  |
| `meta_ads_send_conversions`      | Send offline conversion events via the Conversions API                  | Write  |
| `meta_ads_create_ad`             | Create an ad: single image, single video, carousel, or dynamic creative | Write  |
| `meta_ads_set_status`            | Pause or resume a campaign, ad set, or ad                               | Write  |

Notes:

* Each list tool returns its own collection, a `count`, and `truncated`.
* Insights need `level: "ad"` to attribute results to an individual creative, optionally split by `breakdowns` such as `age` or `publisher_platform`. `meta_ads_read_insights` pages up to 2000 rows and sets `truncated: true` beyond that; each breakdown multiplies the row count.
* Meta cannot edit a creative or swap the one on an ad. Improve one by creating a new ad in the same ad set and pausing the old one; each ad keeps its own insights.
* New ads enter `PENDING_REVIEW` and can come back `DISAPPROVED`. Re-read `effective_status` with `meta_ads_list_ads` before treating an ad as live.
* `meta_ads_set_status` only pauses and resumes. There is no budget tool, so an automation cannot increase spend.

### Creative formats

`meta_ads_create_ad` takes a `creative` object discriminated by `format`, so only the fields belonging to the chosen format are in scope.

| `format`        | Media                                  | Text                                           | Meta `ad_format`   |
| --------------- | -------------------------------------- | ---------------------------------------------- | ------------------ |
| `single_image`  | `imageUrl`                             | `message`, `headline?`, `description?`         | —                  |
| `single_video`  | `videoUrl`, `thumbnailUrl?`            | `message`, `headline?`, `description?`         | —                  |
| `carousel`      | 2-10 `cards`, each image **xor** video | `message` + per-card `headline`                | —                  |
| `dynamic_image` | `imageUrls[]` (1-10)                   | `messages[]`, `headlines[]`, `descriptions[]?` | `SINGLE_IMAGE`     |
| `dynamic_video` | `videoUrls[]` (1-10)                   | `messages[]`, `headlines[]`, `descriptions[]?` | `SINGLE_VIDEO`     |
| `dynamic_mixed` | `imageUrls[]` + `videoUrls[]`          | `messages[]`, `headlines[]`, `descriptions[]?` | `AUTOMATIC_FORMAT` |

Each carousel card is itself discriminated, on `media`:

```ts theme={null}
cards: [
    { media: "image", imageUrl, headline, description?, linkUrl? },
    { media: "video", videoUrl, headline, description?, linkUrl? }
]
```

Shared across formats: `linkUrl`, `callToAction`, `instagramActorId`, `urlTags`, `status`.

Format notes:

* Only the fields you supply are sent, so an omitted `callToAction` leaves Meta's default button. `status` is the exception: it defaults to `PAUSED`, not Meta's `ACTIVE`.
* Carousels containing a video card are Facebook-only: Meta does not support them on Instagram placements.
* Dynamic creative is capped at 30 assets across media and text (`ad_format` and `link_url` each count as one), requires `callToActions`, and only works on an ad set with dynamic creative already enabled in Ads Manager.
* Images are fetched by Meta from the URL you supply; videos and dynamic-creative images are uploaded first, so short-lived signed URLs work everywhere, including the ones Higgsfield returns. The result reports `videoIds` and `imageHashes`.

### Connecting

Open Integrations, choose Meta Ads, and approve the business and assets Meta lists.

Terse uses Facebook Login for Business, which issues a business integration system user token. Unlike a 60-day user token, it does not expire, so scheduled jobs keep running. It is revoked only when the client removes your app under Business settings, Connected apps. Terse checks connections on its maintenance pass and reports a revoked token instead of failing mid-run.

Standard Access, granted automatically, covers every tool above for users with a role on your Meta app. Reaching ad accounts outside your app requires Advanced Access, App Review, and Business Verification. Standard Access rate limits are roughly `300 + 40 x active ads` calls per hour for management and `600 + 400 x active ads` for insights.

## Higgsfield

Generate ad creative images from a text prompt, then animate them into video.

```ts theme={null}
import { Skills } from "./terse.generated"

skills: [Skills.higgsfield()]
```

| Tool                        | Description                                                  | Access |
| --------------------------- | ------------------------------------------------------------ | ------ |
| `higgsfield_generate_image` | Generate one or four images from a text prompt               | Write  |
| `higgsfield_generate_video` | Animate a still image into a video                           | Write  |
| `higgsfield_list_motions`   | List named camera-move presets and their IDs, for `motionId` | Read   |

Notes:

* The tools block until generation finishes. Each asset is copied into Terse storage and returned as a signed URL valid for 24 hours, ready to pass to `meta_ads_create_ad` as an `imageUrl` or `videoUrl`.
* Every call spends Higgsfield credits, so both generators are writes and can be gated behind approval. Use `batchSize: 4` for variants to compare, and match `size` to the placement (`2048x1152` for feed, `1152x2048` for stories).
* `model` trades cost against quality: `dop-lite`, `dop-turbo` (default), `dop-standard`.
* A motion is a named camera move applied on top of your prompt. IDs are opaque, so discover them with `higgsfield_list_motions`, pass one as `motionId`, and scale it with `motionStrength` (0-1, defaults to 1).

There is no text-to-video path, so a video ad is always two calls:

```ts theme={null}
const { images } = await toolbox.higgsfield.generateImage({
    prompt: "A coffee tin on concrete, soft daylight, editorial styling",
    size: "1152x2048"
})
const { videos } = await toolbox.higgsfield.generateVideo({
    imageUrl: images[0].url,
    prompt: "Slow push in, steam rising, shallow depth of field",
    model: "dop-turbo"
})
```

### Connecting

Generate a key in the API keys section of [cloud.higgsfield.ai](https://cloud.higgsfield.ai). (`platform.higgsfield.ai` is the API host, not a sign-up page.) Higgsfield issues a **Key ID** and a **Key Secret**: copy both, and make sure the account has credits.

Paste them as a single colon-joined string, `KEY_ID:KEY_SECRET`, with no spaces, quotes, or the literal `Key` prefix from Higgsfield's header examples. Use the Higgsfield card in the console, or stdin from the CLI so the secret stays out of your shell history:

```bash theme={null}
terse integrate connect higgsfield --fields-stdin <<<'{"credentials":"KEY_ID:KEY_SECRET"}'
```

Terse validates the pair against Higgsfield at connect time and stores it write-only. Rotate it in the Higgsfield dashboard and reconnect if you suspect it leaked.

## Snowflake

Read-only SQL query execution.

```ts theme={null}
skills: [Skills.snowflake()]
```

| Tool                    | Description                                | Access |
| ----------------------- | ------------------------------------------ | ------ |
| `snowflakeExplainQuery` | Explain query execution plan               | Read   |
| `snowflakeExecuteQuery` | Execute SELECT queries (requires approval) | Read   |

## Datadog

Read-only access to logs and RUM events. Optionally scope to specific log indexes.

```ts theme={null}
import { DatadogIndex, Skills } from "./terse.generated"

skills: [Skills.datadog({ indexes: [DatadogIndex.Main] })]
```

`indexes` is optional. Omit it to search across every index the integration can reach.

| Tool                 | Description              | Access |
| -------------------- | ------------------------ | ------ |
| `searchDatadogLogs`  | Search logs              | Read   |
| `listRumEvents`      | List RUM events          | Read   |
| `searchRumEvents`    | Search RUM events        | Read   |
| `aggregateRumEvents` | Aggregate RUM event data | Read   |

## PostHog

Read-only access to product analytics. Supports US PostHog Cloud (`us.posthog.com`) only; EU-hosted PostHog is not supported.

```ts theme={null}
import { PosthogProject, Skills } from "./terse.generated"

skills: [Skills.posthog({ project: PosthogProject.MyTeam })]
```

`terse generate` also emits a `PosthogEventName` union of the custom event names observed in your project over the last 180 days, so `eventName` filters are checked at compile time. Built-in `$`-prefixed events like `$pageview` are always accepted.

| Tool                      | Description                             | Access |
| ------------------------- | --------------------------------------- | ------ |
| `searchPosthogLogs`       | Search logs                             | Read   |
| `searchPosthogSessions`   | Search sessions                         | Read   |
| `getPosthogSessionEvents` | Get events for a session                | Read   |
| `listPosthogEventNames`   | List event names with occurrence counts | Read   |
| `searchPosthogEvents`     | Search events                           | Read   |

## LaunchDarkly

Read-only access to feature flags.

```ts theme={null}
import { LaunchDarklyProject, Skills } from "./terse.generated"

skills: [Skills.launchDarkly({ project: LaunchDarklyProject.MyFlags, environmentKeys: ["production"] })]
```

| Tool                         | Description        | Access |
| ---------------------------- | ------------------ | ------ |
| `listLaunchDarklyFlags`      | List feature flags | Read   |
| `getLaunchDarklyFlagDetails` | Get flag details   | Read   |

## WorkOS

Read-only access to users and organizations.

```ts theme={null}
skills: [Skills.workOS()]
```

| Tool                      | Description        | Access |
| ------------------------- | ------------------ | ------ |
| `listWorkOSUsers`         | List users         | Read   |
| `listWorkOSOrganizations` | List organizations | Read   |
| `getWorkOSUser`           | Get user details   | Read   |

## Apollo

Lead and company enrichment plus prospect search via [Apollo.io](https://www.apollo.io), using your own Apollo API key.

```ts theme={null}
skills: [Skills.apollo()]
```

| Tool                       | Description                                              | Access |
| -------------------------- | -------------------------------------------------------- | ------ |
| `apolloEnrichPerson`       | Enrich a person by Apollo ID, email, or name + domain    | Read   |
| `apolloBulkEnrichPeople`   | Enrich up to 10 people in one call                       | Read   |
| `apolloEnrichOrganization` | Enrich a company by domain (firmographics)               | Read   |
| `apolloSearchPeople`       | Search for people by title, seniority, location, company | Read   |
| `apollo_list_job_postings` | List a company's active job postings as a hiring signal  | Read   |

Credit and key notes:

* Person and organization enrichment consume Apollo **export credits** per matched record. Prefer `apolloBulkEnrichPeople` for lists.
* `apolloSearchPeople` consumes no credits but requires the connected key to be an Apollo **master API key**, and its results never include emails — pass result `id`s to `apolloBulkEnrichPeople` to unlock contact data.
* `apollo_list_job_postings` works with any Apollo key but consumes credits **per page returned** — fetch one large page (up to 500 postings) instead of paging in small steps. Postings carry title/URL/location/date metadata only, no descriptions.
* Personal emails are only returned when `revealPersonalEmails` is set, and Apollo suppresses them for people in GDPR regions.

## Google Search Console

Read Search Analytics and manage the properties and sitemaps of a connected Google account.

```ts theme={null}
skills: [Skills.googleSearchConsole({ sites: [GoogleSearchConsoleSite.DomainAcmeCom] })]
```

`terse generate` writes one `GoogleSearchConsoleSite` static per property the connected account can access, so you reference properties by name instead of pasting identifiers.

| Tool                                           | Description                                               | Access |
| ---------------------------------------------- | --------------------------------------------------------- | ------ |
| `google_search_console_list_sites`             | List every property the connected account can access      | Read   |
| `google_search_console_get_site`               | Get one property and the account's permission level on it | Read   |
| `google_search_console_add_site`               | Add a property to the connected account                   | Write  |
| `google_search_console_delete_site`            | Remove a property from the connected account              | Write  |
| `google_search_console_list_sitemaps`          | List the sitemaps Google knows about for a property       | Read   |
| `google_search_console_get_sitemap`            | Get one sitemap's processing state, errors, and warnings  | Read   |
| `google_search_console_submit_sitemap`         | Submit a sitemap for crawling                             | Write  |
| `google_search_console_delete_sitemap`         | Remove a sitemap submission                               | Write  |
| `google_search_console_query_search_analytics` | Query clicks, impressions, CTR, and position by dimension | Read   |
| `google_search_console_inspect_url`            | Inspect Google's index status for one URL                 | Read   |

Property and data notes:

* A property is either a **URL-prefix** property (`https://example.com/`, trailing slash included) or a **Domain** property (`sc-domain:example.com`).
* The properties you pass to the skill are the only ones the agent may act on, except `google_search_console_list_sites`, which lists everything the account can see. A Domain property also covers its subdomains.
* Search Analytics dates are inclusive `YYYY-MM-DD` in PST and the data lags by roughly 2-3 days, so the most recent days come back empty rather than as zero traffic.
* Query-grouped results omit anonymized queries, so their clicks will not sum to the property total.
* Submitting a sitemap only queues it. Check the outcome later with `google_search_console_get_sitemap`.
* Property access follows the connected Google account, not your Terse organization, so connect the account that actually appears in Search Console.

<Note>
  Search Console uses the sensitive `webmasters` scope. While the OAuth client is in Testing status, only Google accounts on its test-user list can connect, capped at 100. An access-blocked error is
  usually that list.
</Note>

## Web (built-in)

Web search and research tools available to all workflows.

```ts theme={null}
skills: [Skills.web()]
```

Pass `allowedDomains` to restrict the agent to a whitelist of sites. When set, `web_search` results and `web_extract` page fetches are limited to those domains and their subdomains; requests to any other domain are blocked.

```ts theme={null}
skills: [Skills.web({ allowedDomains: ["example.com", "docs.acme.com"] })]
```

| Tool           | Description                             | Access | Respects `allowedDomains` |
| -------------- | --------------------------------------- | ------ | ------------------------- |
| `web_search`   | Search the web                          | Read   | Yes                       |
| `web_extract`  | Extract and process web page content    | Read   | Yes                       |
| `web_research` | Research and synthesize web information | Read   | No (not domain-limited)   |

## Image Edit (built-in)

Edit and generate images.

```ts theme={null}
skills: [Skills.imageEdit()]
```

| Tool         | Description                | Access |
| ------------ | -------------------------- | ------ |
| `image_edit` | Edit and manipulate images | Write  |

## Memory (built-in)

Gives a job a persistent `/memories` directory it can read and write across runs. With the skill added, the agent checks `/memories` before starting and records progress, context, and learnings as it works, so later runs pick up where earlier ones left off.

```ts theme={null}
skills: [Skills.memory()]
```

| Tool     | Description                                                                                                             | Access |
| -------- | ----------------------------------------------------------------------------------------------------------------------- | ------ |
| `memory` | Read, write, edit, and organize files under `/memories` (`view`, `create`, `str_replace`, `insert`, `delete`, `rename`) | Write  |

### What persists between runs

Only files under `/memories` carry over. Every run otherwise starts fresh from your deployed code, so anything written elsewhere in the run is discarded. Anything the agent saves to `/memories` is still there on the next run.

### How memory is organized

* **Per project** — each project has its own isolated memory. Jobs in different projects never share memory.
* **Per job** — within a project, every job gets its own private space; one job cannot see another job's memory.
* **Scoped to `/memories`** — all paths stay inside the `/memories` directory.

Cleanup is automatic: deleting a job removes that job's memory, and deleting a project removes all memory for the project.

`terse test` runs against a separate, isolated test memory, so local testing never touches what your deployed job has saved.

### Inspecting memory

Use the [`terse memory`](/reference/cli#memory) commands to list, read, write, and delete a job's memory files from the terminal. Add `--test` to any of them to target the isolated `terse test` memory instead of production.
