# Sequel Documentation

> The complete Sequel documentation, concatenated as Markdown for LLMs. Sequel is the data layer for your agents: it manages auth, credentials, and execution so AI tools can securely query your cloud databases, warehouses, and SaaS tools over MCP.

Source pages: https://sequel.sh/docs

---

# Getting Started

Connect the Sequel MCP server to your AI tool in a few minutes — the MCP URL, where to get an API key, the two auth types, and how OAuth works.

This guide gets the Sequel MCP server running inside your AI tool. By the end you'll have a connected data source and an agent answering questions against it.

<Steps>
<Step title="Create an account and connect a data source">
Sign up at [sequel.sh](https://sequel.sh) and create or join an organization. Then add a data source — a cloud Postgres or MySQL database, a warehouse like BigQuery, or a SaaS source like Stripe or Google Analytics. See [Connecting your first data source](/docs/connecting-your-first-data-source) for the full walkthrough. Everything you connect becomes available to the MCP server.
</Step>

<Step title="Install the Sequel CLI">
The CLI is the fastest way to wire Sequel into a coding agent — it creates the API key and writes the config for you. One line installs it:

<CodeBlock language="bash">curl -fsSL https://sequel.sh/install | sh</CodeBlock>

Then sign in:

<CodeBlock language="bash">sequel login</CodeBlock>
</Step>

<Step title="Install Sequel into your agent">
Run `install` and pick your agent — Claude Code, Cursor, VS Code, Windsurf, Zed, Codex, or Claude Desktop — or pass its slug directly:

```bash
sequel install          # interactive
sequel install cursor   # or target one directly
```

The CLI runs over the same hosted MCP endpoint every tool uses — `https://api.sequel.sh/mcp` — so you never touch the URL or key by hand. Prefer to configure a tool manually (or using ChatGPT, Claude.ai, or Gemini)? See [Install Sequel MCP](/docs/install) for copy-paste setup.
</Step>

<Step title="Ask your first question">
Restart the agent, then try “What tables do we have, and how many rows are in each?” Go deeper with [Chatting with your data](/docs/chatting-with-your-data).
</Step>
</Steps>

## Authentication: two ways to connect

Sequel supports **two authentication methods** for the MCP server. Pick whichever your tool supports best.

| Method | Best for | How it works |
| --- | --- | --- |
| **API key (Bearer token)** | CLIs and editors — Claude Code, Cursor, VS Code, Windsurf, Zed, Codex, Gemini | You paste a static key into an `Authorization: Bearer …` header in the tool's config. |
| **OAuth 2.0** | Connector-style clients — ChatGPT, Claude.ai | The tool registers itself and sends you through a sign-in + consent screen. No key to copy. |

### API key

Create a key in the Sequel dashboard under **Settings → API Keys**. Keys look like `sql_…` and are shown **once** at creation — copy it then. Add it as a Bearer token:

<CodeBlock language="http" label="Authorization header">Authorization: Bearer sql_your_api_key</CodeBlock>

<Callout type="tip">
When you install from the dashboard, Sequel auto-provisions a key named <code>sequel_mcp</code> and drops it straight into the snippet for you.
</Callout>

### OAuth

For clients that support it, OAuth avoids copying keys around. The flow is standard OAuth 2.0 with PKCE: the client registers itself automatically, you sign in on a Sequel consent screen, and Sequel issues a short-lived access token (refreshed automatically) scoped to your organization. Revoke access any time from the dashboard.

Either method only ever exposes the data sources your organization has connected — see [API keys & authentication](/docs/api-keys-and-auth) for the full details.

## Next step

Head to **[Install Sequel MCP](/docs/install)** for copy-paste setup instructions for every supported AI tool.

---

# Install Sequel MCP

Step-by-step instructions for installing the Sequel MCP server in Claude Code, Cursor, VS Code, ChatGPT, Codex, Windsurf, Zed, and Gemini.

Sequel exposes a single hosted MCP endpoint. Point any MCP-capable AI tool at it and authenticate with your API key:

<CodeBlock language="bash">https://api.sequel.sh/mcp</CodeBlock>

There are two ways to install it into your agent: the **Sequel CLI** (one command — it creates the API key and writes the config for you) or **manual setup** (copy-paste the config into each tool yourself).

## Fastest: the Sequel CLI

The CLI installs Sequel into a coding agent in a single step. It signs you in, provisions a scoped API key for that agent, and writes the right config to the right place — no keys to copy by hand.

<Steps>
<Step title="Install the CLI">
One line installs the latest CLI (`@sequelsh/cli`) with whatever package manager you have:

<CodeBlock language="bash">curl -fsSL https://sequel.sh/install | sh</CodeBlock>

Prefer npm directly? `npm install -g @sequelsh/cli@latest` works too (or `pnpm add -g`, `bun add -g`, `yarn global add`).
</Step>

<Step title="Sign in">
<CodeBlock language="bash">sequel login</CodeBlock>

This opens your browser to authenticate and pick an organization.
</Step>

<Step title="Install Sequel into your agent">
Run `install` and pick an agent from the list, or pass its slug directly:

<CodeBlock language="bash">sequel install</CodeBlock>

The CLI creates an API key named after the agent, then writes its MCP config. For agents that support it, you'll also be asked for a **scope** — `global` (available everywhere on this machine) or `local` (this project only). Pass `--scope` to skip that prompt:

<CodeBlock language="bash">sequel install claude-code --scope global</CodeBlock>

With the slug and `--scope` given, `install` never prompts, so a coding agent can run it for you. Add `--no-browser` (on `login` or `install`) to print the sign-in URL instead of opening a browser.

When `sequel login` is run without a terminal (piped, as a coding agent runs it), it returns straight away with JSON containing the sign-in `url` and finishes the sign-in in the background; `sequel login --status` reports `logged_in`, `pending`, or `failed`, and `sequel login --wait` blocks until a pending sign-in completes. A piped `sequel install` waits for a pending sign-in rather than starting its own.

For agents with a skills directory, the same command also installs the **Sequel skill**: a short `SKILL.md` that tells the agent Sequel's [playbooks](/skills) exist and how to reach them. Every agent gets the playbooks through MCP regardless; the skill file just makes the agent reach for them without being told.

| Agent | Slug | Scopes | Skill file |
| --- | --- | --- | --- |
| Claude Code | `claude-code` | global, local | `~/.claude/skills/sequel/` or `.claude/skills/sequel/` |
| Claude Desktop | `claude` | global | via MCP only |
| Cursor | `cursor` | global, local | via MCP only |
| VS Code | `vscode` | global, local | via MCP only |
| Windsurf | `windsurf` | global | via MCP only |
| Zed | `zed` | global, local | via MCP only |
| Codex | `codex` | global | via MCP only |
| OpenClaw | `openclaw` | global | via MCP only |
| Hermes | `hermes` | global | via MCP only |
</Step>

<Step title="Verify">
Restart the agent and ask it to **"list my Sequel connections."** If it answers, you're connected.
</Step>
</Steps>

<Callout type="tip">
Already have the CLI? `sequel install claude-code` (or any slug above) re-runs setup for that agent at any time.
</Callout>

## Manual setup

Prefer to wire it up yourself — or using ChatGPT, Claude.ai, or Gemini? Generate a key under **Settings → API Keys** in your Sequel dashboard and drop it into the `Authorization` header shown in each snippet below. Some tools — like ChatGPT and Claude.ai — can also sign in with **OAuth** instead of a static key; see [API keys & authentication](/docs/api-keys-and-auth) for the difference.

Expand a tool for its exact setup. Replace `sql_your_api_key` with a real key from your dashboard.

<McpInstallList />

<Callout type="info" title="Using a tool that isn't listed?">
Any client that supports the [Model Context Protocol](https://modelcontextprotocol.io) over streamable HTTP can connect using the URL and `Authorization` header above.
</Callout>

---

# Connecting your first data source

Add a cloud database, warehouse, or SaaS source to Sequel — credentials, testing the connection, and what happens once it's live.

A **data source** is any database, warehouse, or SaaS API you connect to Sequel. Once connected, it's available to the in-app chat and to every AI tool you've authorized through the [MCP server](/docs/install). This guide walks through adding your first one.

<Callout type="info" title="Cloud-hosted sources">
Sequel connects to cloud-hosted data sources — managed Postgres/MySQL (RDS, Supabase, Neon, PlanetScale), warehouses like BigQuery and ClickHouse, and SaaS APIs like Stripe and Google Analytics. Make sure your source is reachable from the internet (or via the access method its guide describes).
</Callout>

## Add a connection

<Steps>
<Step title="Open Connections">
In the Sequel dashboard, go to **Connections** and click **New connection**.
</Step>

<Step title="Pick a source type">
Choose the source you want to connect — for example PostgreSQL, BigQuery, or Google Analytics. Each type asks for the credentials it needs.
</Step>

<Step title="Enter credentials">
Depending on the source, this is a connection string, a host/port/user/password, a service-account key, or an OAuth sign-in. The per-integration guides under [Integrations](/docs/integrations) cover exactly what each source needs.
</Step>

<Step title="Test and save">
Sequel verifies the credentials before saving. A green check means the connection is live and ready to query. If the test fails, double-check network access and credentials.
</Step>
</Steps>

<Callout type="tip" title="Database behind a firewall?">
Sequel reaches your database from a fixed set of IP addresses. If your database sits behind a firewall, security group, or IP allowlist, add Sequel's egress IPs so the connection test can get through. The exact IPs are shown on the connection screen for database sources — copy them into your provider's networking settings (RDS security groups, Supabase/Neon network restrictions, Cloud SQL authorized networks, ClickHouse Cloud IP access list, and the like). SaaS sources that connect over OAuth or an API key don't need this.
</Callout>

## What happens after you connect

- **Schema discovery.** Sequel reads your tables, columns, and relationships so agents can navigate the source without you spelling everything out.
- **Available over MCP.** The source immediately becomes queryable from any AI tool connected to your organization's MCP server.
- **Scoped access.** Connections belong to your organization. API keys and OAuth sessions only ever see the sources your org has connected.

## Connecting more sources

Repeat the flow for every source you want Sequel to reach. Because Sequel can join across connections, adding a second and third source is where it gets powerful — see [Chatting with your data](/docs/chatting-with-your-data) for cross-source examples.

## Next steps

- [Chatting with your data](/docs/chatting-with-your-data)
- [Browse all integrations](/docs/integrations)
- [API keys & authentication](/docs/api-keys-and-auth)

---

# Analyzing data with AI

How to ask good questions, join across sources, and use the Python workbench — whether you're in the Sequel app or an MCP client like Claude or Cursor.

Once a source is connected, you can ask questions in plain English — from the Sequel app or from any AI tool wired up to the [MCP server](/docs/install). This page covers what the agent can do and how to get the most out of it.

## What the agent can do

Behind the scenes, Sequel exposes a small set of tools that the AI agent calls to answer your question:

| Capability | What it's for |
| --- | --- |
| **Discover connections** | List the data sources available to you. |
| **Search schemas** | Find the right tables and columns and plan a query. |
| **Run queries** | Execute read queries and return sample results. |
| **Python workbench** | Run Python (pandas, matplotlib, and more) in an isolated sandbox for analysis and charts. |
| **Download files** | Export results — CSVs, charts — produced in the workbench. |

You don't call these directly. You ask a question; the agent chains the tools to answer it.

## Ask better questions

- **Be specific about the metric and window.** "Weekly active users for the last 90 days" beats "how are we doing."
- **Name the source when it's ambiguous.** "In our Postgres `orders` table…" removes guesswork.
- **Ask for the format you want.** "Return a table" or "plot it as a line chart."

<Callout type="tip">
Start broad to orient the agent ("what tables relate to subscriptions?"), then drill in. The agent keeps context across the conversation.
</Callout>

## Join across sources

This is where Sequel shines. Because every connection is available in one place, a single question can span systems:

> "Compare 90-day retention for users acquired through paid vs organic channels, using signups from Postgres and acquisition source from Google Analytics."

The agent searches both schemas, runs the queries it needs, and combines the results — no manual exports or copy-paste between tools.

## Run real analysis with the workbench

When SQL isn't enough, the agent can run Python in a sandbox:

> "Pull last quarter's order values and plot a histogram with the median marked."

The workbench has common data libraries available and runs in isolation. Generated files (charts, CSVs) can be downloaded directly from the conversation.

## Same data, every tool

The connections you set up power every surface equally — the Sequel app, Claude, Cursor, ChatGPT, and any other MCP client. Set a source up once and ask about it from wherever you work.

## Next steps

- [Install Sequel MCP in your tool](/docs/install)
- [API keys & authentication](/docs/api-keys-and-auth)
- [Browse all integrations](/docs/integrations)

---

# Building with Sequel

Use the Sequel MCP server inside your own agents — examples for the Vercel AI SDK, the Claude Agent SDK, and the OpenAI Agents SDK.

Sequel isn't only for off-the-shelf tools like Claude or Cursor — you can point your own agents at the same hosted MCP server and give them secure, schema-aware access to your data. Every framework that speaks [Model Context Protocol](https://modelcontextprotocol.io) connects the same way: the streamable HTTP endpoint `https://api.sequel.sh/mcp`, authenticated with a Bearer API key.

<Callout type="tip" title="Grab an API key first">
Create a key under **Settings → API Keys** in the dashboard and pass it as `Authorization: Bearer sql_…`. See [API keys & authentication](/docs/api-keys-and-auth) for details.
</Callout>

## Vercel AI SDK

Create an MCP client over the streamable HTTP transport, then hand its tools to `generateText`.

```ts
import { experimental_createMCPClient, generateText, stepCountIs } from "ai";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { anthropic } from "@ai-sdk/anthropic";

const mcp = await experimental_createMCPClient({
  transport: new StreamableHTTPClientTransport(new URL("https://api.sequel.sh/mcp"), {
    requestInit: {
      headers: { Authorization: "Bearer " + process.env.SEQUEL_API_KEY },
    },
  }),
});

const { text } = await generateText({
  model: anthropic("claude-opus-4-8"),
  tools: await mcp.tools(),
  stopWhen: stepCountIs(10),
  prompt: "How many users signed up last week?",
});

console.log(text);
```

## Claude Agent SDK

Register Sequel as an `http` MCP server in the `query` options — the agent discovers and calls the tools on its own.

```ts
import { query } from "@anthropic-ai/claude-agent-sdk";

const response = query({
  prompt: "How many users signed up last week?",
  options: {
    mcpServers: {
      sequel: {
        type: "http",
        url: "https://api.sequel.sh/mcp",
        headers: { Authorization: "Bearer " + process.env.SEQUEL_API_KEY },
      },
    },
  },
});

for await (const message of response) {
  if (message.type === "result") console.log(message.result);
}
```

## OpenAI Agents SDK

Wrap the endpoint in an `MCPServerStreamableHttp` and attach it to your agent's `mcpServers`.

```ts
import { Agent, run, MCPServerStreamableHttp } from "@openai/agents";

const sequel = new MCPServerStreamableHttp({
  name: "Sequel",
  url: "https://api.sequel.sh/mcp",
  requestInit: {
    headers: { Authorization: "Bearer " + process.env.SEQUEL_API_KEY },
  },
});

const agent = new Agent({
  name: "Data analyst",
  instructions: "Answer questions using the connected data sources.",
  mcpServers: [sequel],
});

await sequel.connect();
const result = await run(agent, "How many users signed up last week?");
console.log(result.finalOutput);
await sequel.close();
```

## Next steps

- [Install Sequel MCP in your tool](/docs/install)
- [API keys & authentication](/docs/api-keys-and-auth)
- [Browse all integrations](/docs/integrations)

---

# API keys & authentication

The two ways to authenticate with the Sequel MCP server — static API keys and OAuth 2.0 — plus how to create, scope, and revoke access.

The Sequel MCP server supports **two authentication methods**. Both authorize a client to query the data sources your organization has connected — nothing more. This page explains when to use each and how to manage access.

## At a glance

| | API key | OAuth 2.0 |
| --- | --- | --- |
| **Best for** | CLIs & editors (Claude Code, Cursor, VS Code, Windsurf, Zed, Codex, Gemini) | Connector-style clients (ChatGPT, Claude.ai) |
| **Setup** | Paste a key into the tool's config | Sign in on a consent screen |
| **Credential** | Static `sql_…` Bearer token | Short-lived access token, auto-refreshed |
| **Revoke** | Delete the key | Revoke the authorization |

## API keys

API keys are static Bearer tokens you generate once and paste into a tool's MCP config.

<Steps>
<Step title="Create a key">
Go to **Settings → API Keys** in the Sequel dashboard and create a new key. Give it a descriptive name (e.g. the tool or person it's for).
</Step>

<Step title="Copy it immediately">
The full key is shown **once**, at creation. Copy it then — afterward only a short prefix is visible for identification.
</Step>

<Step title="Use it as a Bearer token">
Add it to your tool's MCP config:

<CodeBlock language="http" label="Authorization header">Authorization: Bearer sql_your_api_key</CodeBlock>
</Step>
</Steps>

<Callout type="tip" title="The sequel_mcp key">
When you install Sequel from the dashboard's install dialog, it auto-provisions a key named <code>sequel_mcp</code> and inserts it into the config snippet for you — so you usually don't have to create one by hand.
</Callout>

<Callout type="warning" title="Keep keys private">
A key grants access to your organization's connected data. Don't commit keys to shared repos — give each person or tool its own key, and revoke any key that may have leaked.
</Callout>

## OAuth 2.0

For clients that support it, OAuth avoids sharing static keys. Sequel implements standard **OAuth 2.0 with PKCE**:

<Steps>
<Step title="The client registers itself">
The MCP client registers with Sequel automatically (dynamic client registration) — there's nothing to paste.
</Step>

<Step title="You sign in and consent">
You're redirected to a Sequel consent screen, sign in, and pick the organization to authorize.
</Step>

<Step title="Sequel issues tokens">
Sequel returns a short-lived access token (refreshed automatically in the background) scoped to your organization. The client uses it on every request.
</Step>
</Steps>

To use OAuth, just choose Sequel as an MCP connector in a client that supports it (for example ChatGPT's Connectors or Claude.ai integrations) and follow the sign-in prompt.

## Scope & revocation

- **Organization-scoped.** Every key and OAuth session can only reach the data sources your organization has connected.
- **Revoke anytime.** Delete an API key, or revoke an OAuth authorization, from the dashboard. Access stops immediately.
- **Rotate freely.** Issue separate keys per tool or environment so you can revoke one without disrupting the others.

## Next steps

- [Install Sequel MCP in your tool](/docs/install)
- [Getting Started](/docs/getting-started)

---

# Integrations

Connect Sequel to your databases, warehouses, product analytics, and SaaS tools. Browse every supported integration and its setup guide.

Sequel connects to cloud databases, warehouses, product analytics, and SaaS APIs — and can join across them in a single question. Pick a source below for its step-by-step setup guide.

<Callout type="info">
Guides are rolling out continuously. Sources without a dedicated guide yet still work today via the [connections flow](/docs/connecting-your-first-data-source).
</Callout>

<ConnectorGrid />

---

# How to connect PostgreSQL to AI agents?

Connect a cloud-hosted PostgreSQL database to Sequel and query it in natural language from the app or any MCP client.

Connect a cloud-hosted PostgreSQL database to Sequel and query it in plain English — from the Sequel app or any AI tool wired up to the [MCP server](/docs/install). This works with managed Postgres anywhere: Amazon RDS/Aurora, Supabase, Neon, Render, Railway, Heroku, Digital Ocean, and similar.

## What you'll need

- A **cloud-hosted PostgreSQL** database reachable over the internet.
- A database **connection string** (or host, port, database, user, and password).
- A user with **read access** to the schemas you want to query.

<Callout type="tip" title="Use a read-only user">
Create a dedicated read-only role for Sequel. It's the simplest way to guarantee queries can't modify your data, and it scopes access to just the tables you intend to expose.
</Callout>

## Connect it

<Steps>
<Step title="Open a new connection">
In the Sequel dashboard, go to **Connections → New connection** and choose **PostgreSQL**.
</Step>

<Step title="Enter your connection string">
Paste your connection string. It usually looks like this:

<CodeBlock language="bash" label="Connection string">postgresql://USER:PASSWORD@HOST:5432/DATABASE?sslmode=require</CodeBlock>

Most managed providers require SSL — keep `sslmode=require` unless your provider says otherwise.
</Step>

<Step title="Allow Sequel through your firewall">
Sequel connects to your database from a fixed set of IP addresses. If your database (or its provider's firewall/security group) restricts inbound traffic, add Sequel's egress IPs to the allowlist so the connection test can reach it.

The exact IPs to allowlist are shown on the connection screen — copy them from there into your provider's networking settings (e.g. RDS security groups, Supabase network restrictions, Neon IP allow).
</Step>

<Step title="Test and save">
Sequel verifies the credentials and reads your schema. A green check means it's live.
</Step>
</Steps>

## Try it

Once connected, ask things like:

- "How many rows are in each table?"
- "Show me the 10 most recently created users."
- "What foreign keys reference the `orders` table?"
- "Plot daily signups for the last 30 days."

## Troubleshooting

| Symptom | Likely fix |
| --- | --- |
| Connection test times out | Database isn't reachable from the internet, or a firewall is blocking Sequel — add Sequel's egress IPs (shown on the connection screen) to your allowlist. |
| SSL/TLS error | Add or adjust `sslmode` (commonly `require`) to match your provider. |
| "Permission denied" on a table | The user lacks `SELECT` on that table or schema — grant read access. |

## Related

- [Connecting your first data source](/docs/connecting-your-first-data-source)
- [Chatting with your data](/docs/chatting-with-your-data)
- [Browse all integrations](/docs/integrations)

---

# How to connect MySQL to AI agents?

Connect a cloud-hosted MySQL database to Sequel and query it in natural language from the app or any MCP client.

Connect a cloud-hosted MySQL database to Sequel and query it in plain English — from the Sequel app or any AI tool wired up to the [MCP server](/docs/install). This works with managed MySQL anywhere: PlanetScale, Amazon RDS/Aurora MySQL, Google Cloud SQL, Azure Database for MySQL, and similar (MySQL 5.7+, including 8.x, and MariaDB 10.3+).

## What you'll need

- A **cloud-hosted MySQL** database reachable over the internet.
- A database **connection string** (host, port, database, user, and password).
- A user with **read access** to the schemas you want to query.

<Callout type="tip" title="Use a read-only user">
Create a dedicated read-only user for Sequel. It's the simplest way to guarantee queries can't modify your data, and it scopes access to just the tables you intend to expose.
</Callout>

## Connect it

<Steps>
<Step title="Open a new connection">
In the Sequel dashboard, go to **Connections → New connection** and choose **MySQL**.
</Step>

<Step title="Enter your connection string">
Paste your connection string. It usually looks like this:

<CodeBlock language="bash" label="Connection string">mysql://USER:PASSWORD@HOST:3306/DATABASE</CodeBlock>

Most managed providers require SSL — append the SSL parameters your provider documents (some need a CA certificate) if the connection is rejected without them.
</Step>

<Step title="Allow Sequel through your firewall">
Sequel connects to your database from a fixed set of IP addresses. If your database (or its provider's firewall/security group) restricts inbound traffic, add Sequel's egress IPs to the allowlist so the connection test can reach it.

The exact IPs to allowlist are shown on the connection screen — copy them from there into your provider's networking settings (e.g. RDS security groups, Cloud SQL authorized networks, PlanetScale/Azure IP allow).
</Step>

<Step title="Test and save">
Sequel verifies the credentials and reads your schema. A green check means it's live.
</Step>
</Steps>

## Try it

Once connected, ask things like:

- "Show me total sales by product category last quarter"
- "Which customers haven't placed an order in 90 days?"
- "Show me the top 20 users by total spend"

## Troubleshooting

| Symptom | Likely fix |
| --- | --- |
| Connection test times out | Database isn't reachable from the internet, or a firewall is blocking Sequel — add Sequel's egress IPs (shown on the connection screen) to your allowlist. |
| SSL/TLS connection error | Provider requires an encrypted connection — add the SSL parameters (and CA certificate, if needed) from your provider's docs. |
| Access denied for user | The user lacks a `SELECT` grant on the target database, or the credentials are wrong. |

## Related

- [Connecting your first data source](/docs/connecting-your-first-data-source)
- [Chatting with your data](/docs/chatting-with-your-data)
- [Browse all integrations](/docs/integrations)

---

# How to connect ClickHouse to AI agents?

Connect a cloud-hosted ClickHouse database to Sequel and query it in natural language from the app or any MCP client.

Connect a cloud-hosted ClickHouse database to Sequel and ask analytical questions in plain English — from the Sequel app or any AI tool wired up to the [MCP server](/docs/install). This works with ClickHouse Cloud and self-managed ClickHouse running on a host Sequel can reach over the internet.

## What you'll need

- A **cloud-hosted ClickHouse** server reachable over the internet (ClickHouse Cloud or self-managed).
- The **host**, **port**, **database**, **username**, and **password** for the server.
- A user with **read access** to the databases you want to query.

<Callout type="tip" title="Use a read-only user">
Create a dedicated read-only user for Sequel. It's the simplest way to guarantee queries can't modify your data, and it scopes access to just the tables you intend to expose.
</Callout>

## Connect it

<Steps>
<Step title="Open a new connection">
In the Sequel dashboard, go to **Connections → New connection** and choose **ClickHouse**.
</Step>

<Step title="Enter your connection details">
Fill in each field:

<CodeBlock language="bash" label="Connection details">host: your-instance.clickhouse.cloud
port: 8443        # ClickHouse Cloud. Self-managed HTTP is usually 8123
username: default
password: ••••••••
database: default</CodeBlock>

For ClickHouse Cloud, use the secure HTTP port **8443**. Self-managed servers typically expose plain HTTP on **8123**.
</Step>

<Step title="Allow Sequel through your firewall">
Sequel connects to your server from a fixed set of IP addresses. If your server restricts inbound traffic, add Sequel's egress IPs to the allowlist so the connection test can reach it. In ClickHouse Cloud, add them under **Settings → IP Access List**.

The exact IPs to allowlist are shown on the connection screen — copy them from there.
</Step>

<Step title="Test and save">
Sequel pings the server, verifies the credentials, and reads your schema. A green check means it's live.
</Step>
</Steps>

## What you can query

Once connected, Sequel can list the databases on your server, read every table's columns and types, and run read-only SQL against them — so you can ask questions across any database you've granted access to.

## Try it

Once connected, ask things like:

- "How many events did we receive per hour yesterday?"
- "What's the funnel conversion rate from signup to first purchase?"
- "Show me the top 10 users by event count this week."
- "Which event types have increased most in the last 7 days?"

## Troubleshooting

| Symptom | Likely fix |
| --- | --- |
| Connection test times out | The server isn't reachable from the internet, or a firewall is blocking Sequel — add Sequel's egress IPs (shown on the connection screen) to your allowlist. In ClickHouse Cloud, that's **Settings → IP Access List**. |
| Authentication failed | Double-check the username and password, and confirm the user is allowed to connect from Sequel's network. |
| Empty schema or "unknown database" | Verify the **database** name and make sure the user has read access to the tables you want to query. |

## Related

- [Connecting your first data source](/docs/connecting-your-first-data-source)
- [Chatting with your data](/docs/chatting-with-your-data)
- [Browse all integrations](/docs/integrations)

---

# How to connect Google BigQuery to AI agents?

Connect Google BigQuery to Sequel and query petabytes of data in natural language from the app or any MCP client.

Connect Google BigQuery to Sequel and query it in plain English — from the Sequel app or any AI tool wired up to the [MCP server](/docs/install). BigQuery is Google's fully-managed, serverless data warehouse, and Sequel connects to it with a read-only Google sign-in so you can ask questions across your datasets without writing GoogleSQL by hand.

## What you'll need

- A **Google Cloud project** with the **BigQuery API** enabled.
- A **Google account** with read access to the datasets you want to query — in practice the `roles/bigquery.dataViewer` role (to read tables) and `roles/bigquery.jobUser` role (to run queries).
- Your **GCP Project ID** — the unique identifier for your project, e.g. `my-company-analytics-123`. You'll find it at the top of the Google Cloud Console, or by running `gcloud config get-value project`.

<Callout type="tip" title="Connect with a least-privilege account">
Sign in with an account that only has read access to the datasets you intend to expose. Sequel requests the read-only `bigquery.readonly` scope, so it can run `SELECT` queries but can never modify your data.
</Callout>

## Connect it

<Steps>
<Step title="Open a new connection">
In the Sequel dashboard, go to **Connections → New connection** and choose **BigQuery**.
</Step>

<Step title="Sign in with Google">
Authorize Sequel through Google OAuth 2.0. Sequel only requests the read-only `bigquery.readonly` scope — there are no service-account JSON keys to create or manage.
</Step>

<Step title="Enter your GCP Project ID">
Provide the project you want to query.

<CodeBlock language="bash" label="GCP Project ID">my-company-analytics-123</CodeBlock>

Sequel verifies the project, confirms the BigQuery API is enabled, and checks that your account can list datasets.
</Step>

<Step title="Test and save">
Sequel reads your datasets and table schemas. A green check means it's live. You can restrict schema introspection to specific datasets later from the connection settings.
</Step>
</Steps>

## Try it

Once connected, ask things like:

- "What are the top 10 events by count this week?"
- "Show me daily revenue for the past 30 days as a line chart."
- "Which users had the highest session count last month?"
- "List all tables in my analytics dataset with row counts."

## Troubleshooting

| Symptom | Likely fix |
| --- | --- |
| "Access denied" / 403 | Make sure the BigQuery API is enabled and your account has `bigquery.dataViewer` (read) and `bigquery.jobUser` (run queries) roles. |
| "Project not found" / 404 | Double-check the GCP Project ID — use the project ID, not the project name or number. |
| Queries scan too many bytes | BigQuery bills per bytes scanned. Add an instruction in the connection settings to always filter by partition columns on large tables. |

## Related

- [Connecting your first data source](/docs/connecting-your-first-data-source)
- [Chatting with your data](/docs/chatting-with-your-data)
- [Browse all integrations](/docs/integrations)

---

# How to connect Cloudflare D1 to AI agents?

Connect Cloudflare D1 to Sequel and ask your serverless SQLite database questions in natural language, from the app or any MCP client.

Connect Cloudflare D1 to Sequel and query it in plain English, from the Sequel app or any AI tool wired up to the [MCP server](/docs/install). D1 is Cloudflare's serverless SQLite database, and Sequel reaches it over Cloudflare's D1 API, so there is nothing to install and no tunnel or IP allowlist to configure.

The interesting part is usually not D1 on its own. D1 holds your product data, while the rest of the answer lives in billing, CRM, and analytics. Once D1 is connected you can ask one question that spans all of them and let Sequel work out which source answers which part.

## What you'll need

- A **Cloudflare account** with at least one **D1 database**.
- Permission to authorize applications on that account. If your account administrator has turned off third-party OAuth access under **Manage Account → Members → Settings**, you will need them to re-enable it.

<Callout type="note" title="One connection, one database">
A Sequel connection is pinned to exactly one D1 database, chosen when you connect. Sequel has no way to list or query any other database on your account through that connection. To query a second database, add a second connection.
</Callout>

## Connect it

<Steps>
<Step title="Open a new connection">
In the Sequel dashboard, go to **Connections → New connection** and choose **Cloudflare D1**.
</Step>

<Step title="Sign in with Cloudflare">
You'll be sent to Cloudflare's consent screen. Approve the connection to give Sequel read access to your D1 databases. There is no API token to create, paste, or rotate, and you can revoke access at any time.
</Step>

<Step title="Pick a database">
Sequel lists the D1 databases you just authorized, grouped by account. Choose the one this connection should use.

Sequel stores the account and database you picked, so subsequent queries always go to that database and nowhere else.
</Step>

<Step title="Test and save">
Sequel reads your table and column definitions from `sqlite_master` and `PRAGMA table_info`, then writes a short summary of what the database contains. A green check means it's live.
</Step>
</Steps>

## Try it

Once connected, ask things like:

- "How many rows are in each table?"
- "Show me the 20 most recently created users."
- "Plot daily order volume for the last 30 days."
- "What's the conversion rate from signup to first purchase, by month?"
- "Which customers in D1 also have an active Stripe subscription?"

Because D1 is SQLite, joins, CTEs, window functions, and JSON1 queries all work the way you'd expect.

## How Sequel reads your schema

Sequel introspects the database once at connect time, and again when you refresh the schema:

- Tables come from `sqlite_master`, excluding SQLite's own `sqlite_*` tables and D1's internal `_cf_*` bookkeeping tables.
- Columns come from `PRAGMA table_info`, which supplies the type, `NOT NULL` status, default value, and primary-key flag for each one.

That schema is summarized and stored with the connection, so the agent knows your real column names before it writes any SQL.

## What Sequel can and can't do

Sequel requests four read-only scopes and nothing else:

| Scope | What it's for |
| --- | --- |
| `d1.read` | Read your D1 databases and run `SELECT` queries |
| `memberships.read` | List which Cloudflare accounts you belong to |
| `account-settings.read` | Resolve those accounts for the database picker |
| `user-details.read` | Identify the signed-in user |

Read-only is enforced by Cloudflare, not just by convention on our side. Because Sequel holds only `d1.read`, a write that somehow reached the D1 API would be rejected by Cloudflare itself. Sequel requests no Workers, DNS, or zone access at all.

You can revoke the grant at any time from your Cloudflare profile under Access Management.

## Troubleshooting

| Symptom | Likely fix |
| --- | --- |
| No databases in the picker | The authorized account has no D1 databases yet, or you approved a different account than you expected. Create a database, or reconnect and pick the right account. |
| "Cloudflare has revoked access for this connection" | The grant was revoked, or an administrator disabled third-party OAuth for the account. Reconnect from Sequel once access is restored. |
| Consent screen won't let you approve | Your account administrator has blocked public OAuth app access under **Manage Account → Members → Settings**. |
| "no such table" | The table may have been created after you connected. Refresh the schema from the connection settings. |
| A table is missing from the schema | Tables named `sqlite_*` or `_cf_*` are filtered out deliberately. Anything else should appear after a schema refresh. |
| A write query fails | Expected. Sequel holds only the `d1.read` scope, so Cloudflare rejects writes. |

## Related

- [Connecting your first data source](/docs/connecting-your-first-data-source)
- [Chatting with your data](/docs/chatting-with-your-data)
- [Browse all integrations](/docs/integrations)

---

# How to connect Google Analytics to AI agents?

Connect Google Analytics 4 to Sequel with OAuth and ask about sessions, conversions, and traffic in natural language.

Connect **Google Analytics 4 (GA4)** to Sequel and ask about your traffic, sessions, conversions, and acquisition channels in plain English — then join it with your product or revenue data for questions a single tool can't answer alone.

## What you'll need

- A **Google account** with access to the GA4 property you want to query.
- At least **Viewer** access on that property.

Google Analytics connects over **OAuth**, so there's no key to copy — you authorize Sequel with your Google account and pick the property.

## Connect it

<Steps>
<Step title="Open a new connection">
In the Sequel dashboard, go to **Connections → New connection** and choose **Google Analytics**.
</Step>

<Step title="Sign in with Google">
You'll be redirected to Google's consent screen. Sign in and grant Sequel read access to your Analytics data.
</Step>

<Step title="Pick a property">
Choose the GA4 property you want Sequel to query. You can connect additional properties later by repeating this flow.
</Step>

<Step title="Confirm">
Sequel verifies access and the connection goes live.
</Step>
</Steps>

<Callout type="info" title="GA4, not Universal Analytics">
Sequel queries GA4 properties. Universal Analytics properties stopped processing data in 2023 and aren't supported.
</Callout>

## Try it

- "How many sessions did we get last week, by channel?"
- "What were our top 10 landing pages by users this month?"
- "Compare conversion rate for mobile vs desktop over the last 30 days."
- "Which acquisition source drove the most new users this quarter?"

## Join it with other sources

Google Analytics gets more powerful alongside your other data. With a database also connected, ask:

> "For users acquired through paid search in GA, what's their 90-day retention in our Postgres `events` table?"

## Troubleshooting

| Symptom | Likely fix |
| --- | --- |
| No properties listed | The Google account you authorized doesn't have access to the property — use one with at least Viewer access. |
| Authorization fails | Re-run the OAuth flow and make sure you grant the requested Analytics read scope. |
| Data looks empty | Confirm the property actually has data in the date range you're asking about. |

## Related

- [Google Search Console](/docs/google-search-console)
- [Chatting with your data](/docs/chatting-with-your-data)
- [Browse all integrations](/docs/integrations)

---

# How to connect Amplitude to AI agents?

Connect Amplitude to Sequel with an API key and ask about events, active users, retention, and funnels in natural language.

Connect **Amplitude** to Sequel and ask about your product analytics — events, active users, retention, funnels, and cohorts — in plain English, from the Sequel app or any AI tool wired up to the [MCP server](/docs/install). Then join it with your warehouse, revenue, or CRM data for questions a single tool can't answer alone.

## What you'll need

- An **Amplitude account** with access to the project you want to query.
- Your project's **API Key** and **Secret Key**, found under **Settings → Projects → [your project] → General**.
- The **region** your project lives in — **US** (amplitude.com) or **EU** (analytics.eu.amplitude.com).

<Callout type="tip" title="Where to find your keys">
In Amplitude, open **Settings → Projects**, pick your project, and the **General** tab shows the **API Key** and **Secret Key**. Read access to the project is enough — Sequel only uses these keys for read-only analytics queries.
</Callout>

## Connect it

<Steps>
<Step title="Copy your API & Secret Key">
In Amplitude, go to **Settings → Projects → [your project] → General** and copy both the **API Key** and **Secret Key**.
</Step>

<Step title="Open a new connection">
In the Sequel dashboard, go to **Connections → New connection**, choose **Amplitude**, and give it a name.
</Step>

<Step title="Paste your keys and pick a region">
Paste the **API Key** and **Secret Key**, then select your **region** (US or EU) — the one the project was created in. Sequel combines the two keys as HTTP Basic auth against Amplitude's read APIs.
</Step>

<Step title="Test & save">
Click **Test**. Sequel validates the keys against the Taxonomy API. When it confirms, save the connection and it goes live.
</Step>
</Steps>

## What you can query

Sequel exposes read-only tools over Amplitude's Taxonomy, behavioral query, and Dashboard APIs:

- **Events catalog** — list every event tracked in the project, including last-seen timestamps.
- **Event properties** — the full property list (and value samples) for any event.
- **Event segmentation** — counts, uniques, sums, and averages over any event, grouped or filtered by property.
- **Funnel reports** — conversion and drop-off across any step sequence and window.
- **Retention reports** — N-day, weekly, and monthly retention by cohort.
- **Saved charts** — re-pull an existing Amplitude chart by its chart ID and re-segment it.

The connector is read-only; it cannot create or modify events, charts, or cohorts.

## Try it

- "What's the 7-day retention for users who signed up last month?"
- "Show me the conversion funnel from signup to first paid action over the past 30 days."
- "Which onboarding step has the biggest drop-off?"
- "How many users triggered 'Subscription Started' last week, by plan?"

## Join it with other sources

> "Compare onboarding funnel conversion in Amplitude against MRR from Stripe by signup month."

## Troubleshooting

| Symptom | Likely fix |
| --- | --- |
| Test fails with a 401 / auth error | The API Key or Secret Key is wrong. Re-copy both from **Settings → Projects → General** — they're easy to swap. |
| Connects but returns empty results | The region is almost always the culprit. Switch between **US** and **EU** to match where the project was created, then re-test. |
| A report comes back with no data | No events fired in the date range. Widen the range or confirm the event name with a quick "list events" question. |

## Related

- [Connecting your first data source](/docs/connecting-your-first-data-source)
- [Chatting with your data](/docs/chatting-with-your-data)
- [Browse all integrations](/docs/integrations)

---

# How to connect Mixpanel to AI agents?

Connect Mixpanel to Sequel with a Service Account and ask about your product analytics — events, funnels, retention, and active users — in natural language.

Connect **Mixpanel** to Sequel and ask about your product analytics in plain English — events, funnels, retention, segmentation, and active users — from the Sequel app or any AI tool wired up to the [MCP server](/docs/install). Skip the drop-downs: describe the report you want and Sequel runs it against Mixpanel's Query API. And because it's just another source, you can join your Mixpanel data with the rest of your stack in a single question. Sequel only ever reads.

## What you'll need

- A **Mixpanel account** with a project you can read.
- A **Service Account username** — looks like `my-sa.abc123.mp-service-account`.
- The **Service Account secret** — shown once when the account is created.
- Your numeric **project ID** — under **Project Settings → Overview**, or the digits after `/project/` in any Mixpanel URL.
- The project's **region** — `us` (mixpanel.com) or `eu` (eu.mixpanel.com). Pick the region the project was created in.

<Callout type="tip" title="Create a Service Account in Mixpanel">
Go to **Organization Settings → Service Accounts** and create a new account with at least **project-level read access** — a read role is enough, since Sequel never writes. Copy the username and the secret (the secret is shown only once) before leaving the page.
</Callout>

## Connect it

<Steps>
<Step title="Open a new connection">
In the Sequel dashboard, go to **Connections → New connection** and choose **Mixpanel**.
</Step>

<Step title="Create a Service Account in Mixpanel">
In Mixpanel, go to **Organization Settings → Service Accounts** and create an account with project-level read access. Copy the **username** and the **secret** shown at creation.
</Step>

<Step title="Fill in the connection form">
Paste the **Service Account username** and **secret**, enter your numeric **project ID**, and choose the **region** (`us` or `eu`) the project lives in. Sequel uses the username and secret as HTTP Basic auth.
</Step>

<Step title="Test and save">
Sequel pings your project against the Mixpanel API. A green check means it's live and ready to query.
</Step>
</Steps>

## What you can query

Sequel exposes Mixpanel as read-only tools over the Query API:

- **Events** — list the event names tracked in the project.
- **Event properties** — list the properties on a given event.
- **Segmentation** — counts, uniques, sums, and averages over any event, grouped by any property, bucketed by minute, hour, day, week, or month.
- **Funnels** — list saved funnels, or run conversion and drop-off across any custom step sequence and window.
- **Raw event export** — pull row-level events bounded by date range and event name.

<Callout type="info" title="Read-only">
The connector ships query and export tools only — it can't create or modify reports, funnels, or cohorts. Raw export is rate-limited (60 queries/hour/project), so keep its date range narrow and lean on segmentation for aggregate questions.
</Callout>

## Try it

- "How many users completed 'Subscription Started' last week, by plan?"
- "Show me the signup → activation funnel conversion for the past 30 days."
- "Compare DAU for iOS vs Android over the last 60 days."
- "Which step in funnel 4321 has the biggest drop-off?"

## Join it with other sources

Mixpanel is most useful next to your other data. With Stripe and Postgres also connected, ask:

> "Match users who completed activation in Mixpanel against paying customers in Stripe, and pull their plan and signup date from Postgres."

## Troubleshooting

| Symptom | Likely fix |
| --- | --- |
| `401 Unauthorized` on connect | Re-check the Service Account username and secret for typos. If the secret was lost (it's shown only once), create a new Service Account and use the fresh credentials. |
| Connects but returns no data | Confirm the **project ID** and **region** match — a US project queried against the EU host (or vice versa) returns 401s or empty results. |
| A report comes back empty | Loosen filters and confirm there are events in the date range; dates are interpreted in the project's timezone. |

## Related

- [Connecting your first data source](/docs/connecting-your-first-data-source)
- [Chatting with your data](/docs/chatting-with-your-data)
- [Browse all integrations](/docs/integrations)

---

# How to connect PostHog to AI agents?

Connect PostHog to Sequel and ask about events, funnels, retention, and active users in plain English — HogQL under the hood.

Connect **PostHog** to Sequel and ask about your product analytics in plain English — events, funnels, retention, active users, cohorts, and saved insights — from the Sequel app or any AI tool wired up to the [MCP server](/docs/install). Sequel writes HogQL (PostHog's SQL dialect) under the hood, only ever reads, and can join PostHog with your other connected sources.

## What you'll need

- A **PostHog account** (PostHog Cloud or self-hosted).
- A **Personal API key** with read-only scopes: `query:read`, `event_definitions:read`, `property_definitions:read`, `insight:read`, `cohort:read`, `project:read`.
- Your **host** — `https://us.posthog.com` for US Cloud (default), `https://eu.posthog.com` for EU Cloud, or your self-hosted base URL (no trailing slash).
- Your numeric **project ID** — found in your PostHog URL: `app.posthog.com/project/<id>`.

<Callout type="tip" title="Read-only by design">
The PostHog connector ships only read tools (HogQL query, list events, list properties, list insights, list cohorts). Granting the Personal API key read-only scopes means Sequel can't create or change insights, dashboards, cohorts, or feature flags — even in principle. Rotate or revoke the key from PostHog Settings at any time.
</Callout>

## Connect it

<Steps>
<Step title="Open a new connection">
In the Sequel dashboard, go to **Connections → New connection** and choose **PostHog**.
</Step>

<Step title="Create a Personal API key">
In PostHog, go to **Settings → Personal API Keys → Create personal API key**. Grant the read-only scopes: `query:read`, `event_definitions:read`, `property_definitions:read`, `insight:read`, `cohort:read`, and `project:read`. Copy the key — PostHog only shows it once.
</Step>

<Step title="Enter your key, host, and project ID">
Paste the **API key** into Sequel, set the **host** (US Cloud, EU Cloud, or your self-hosted base URL), and enter your numeric **project ID** from the PostHog URL.
</Step>

<Step title="Test and save">
Sequel verifies the credentials against your project and confirms the connection by name. A green check means it's live.
</Step>
</Steps>

## What you can query

Sequel exposes PostHog's product-analytics data as read-only tools:

- **HogQL queries** — arbitrary SQL against the `events`, `persons`, `sessions`, `groups`, and `cohort_people` tables for counts, funnels, retention, MAU/DAU, and property breakdowns.
- **Event definitions** — event names with first-/last-seen timestamps and 30-day volume, so the agent uses the events you actually track.
- **Property definitions** — event and person properties with type info to compose accurate HogQL.
- **Insights** — saved trends, funnels, and retention insights the team has already built.
- **Cohorts** — saved user segments, referenceable from HogQL as `person_id IN COHORT <id>`.

## Try it

- "How many unique users triggered $pageview yesterday, by country?"
- "Show me the signup → activation funnel for users created in the last 30 days."
- "What's the 7-day retention of users in the 'Power users' cohort?"
- "Show me daily active users, broken down by self-serve vs sales-led signup."

## Join it with other sources

PostHog is most powerful next to your accounts and revenue data. With Postgres also connected, ask:

> "For accounts in our Postgres `customers` table on the Pro plan, what's their 30-day active usage of the checkout flow in PostHog?"

## Troubleshooting

| Symptom | Likely fix |
| --- | --- |
| Invalid or unauthorized key | Re-check the key for typos, or create a fresh Personal API key in PostHog with the required read scopes and paste it again. |
| Connection fails or returns the wrong project | Confirm the **host** matches your region — `us.posthog.com` vs `eu.posthog.com` (or your self-hosted base URL, no trailing slash). |
| Queries return no data | Verify the numeric **project ID** is correct and that data exists in the date range you're asking about. |

## Related

- [Connecting your first data source](/docs/connecting-your-first-data-source)
- [Chatting with your data](/docs/chatting-with-your-data)
- [Browse all integrations](/docs/integrations)

---

# How to connect Google Search Console to AI agents?

Connect Google Search Console to Sequel with OAuth and ask about clicks, impressions, queries, and pages in natural language.

Connect **Google Search Console (GSC)** to Sequel and ask about your organic search performance — clicks, impressions, average position, top queries, and top pages — in plain English. Pair it with Google Analytics and your product data for a full picture of how search traffic converts.

## What you'll need

- A **Google account** with access to the Search Console property you want to query.
- At least **Restricted** or **Full** user access on that property.

Search Console connects over **OAuth** — authorize Sequel with your Google account and select the property.

## Connect it

<Steps>
<Step title="Open a new connection">
In the Sequel dashboard, go to **Connections → New connection** and choose **Google Search Console**.
</Step>

<Step title="Sign in with Google">
Grant Sequel read access on Google's consent screen.
</Step>

<Step title="Pick a property">
Choose the verified Search Console property (a domain or URL-prefix property) you want to query.
</Step>

<Step title="Confirm">
Sequel verifies access and the connection goes live.
</Step>
</Steps>

<Callout type="info" title="Data freshness">
Search Console data typically lags by a couple of days, and very recent dates may be incomplete. Ask about ranges that end a few days back for stable numbers.
</Callout>

## Try it

- "What were our top 20 search queries by clicks last month?"
- "Which pages gained the most impressions week over week?"
- "What's our average position for branded vs non-branded queries?"
- "Show clicks and CTR by country for the last 28 days."

## Join it with other sources

> "Take our top 20 GSC landing pages by clicks and show how many of those sessions converted, using our Postgres `orders` table."

## Troubleshooting

| Symptom | Likely fix |
| --- | --- |
| No properties listed | The authorized account lacks access — use one added as a user on the property in Search Console. |
| Recent dates empty | GSC data lags; query a range ending 2–3 days ago. |
| Authorization fails | Re-run the OAuth flow and grant the requested Search Console read scope. |

## Related

- [Google Analytics](/docs/google-analytics)
- [Chatting with your data](/docs/chatting-with-your-data)
- [Browse all integrations](/docs/integrations)

---

# How to connect Ahrefs to AI agents?

Connect Ahrefs to Sequel with an API key and ask about backlinks, keywords, rankings, and organic traffic in natural language.

Connect **Ahrefs** to Sequel and ask about your SEO data — backlinks, organic keywords, rankings, domain metrics, and organic traffic — in plain English, from the Sequel app or any AI tool wired up to the [MCP server](/docs/install). Then join it with your analytics or revenue data for questions a single tool can't answer alone.

## What you'll need

- An **Ahrefs account** with API access (workspace owners and admins can create keys).
- An **Ahrefs API key**, created under **Account settings → API keys**.

<Callout type="info" title="The Ahrefs API needs an eligible plan">
The Ahrefs API is available on eligible paid plans. On other plans you still get a limited set of free test queries. Most reports also consume Ahrefs API units (minimum 50 per request) billed against your plan's allowance — validating your key is free. See Ahrefs' pricing page for current API eligibility.
</Callout>

## Connect it

<Steps>
<Step title="Create an API key in Ahrefs">
In Ahrefs, go to **Account settings → API keys** and create a new key. Only workspace owners and admins can do this. Copy the key.
</Step>

<Step title="Open a new connection">
In the Sequel dashboard, go to **Connections → New connection**, choose **Ahrefs**, and give it a name.
</Step>

<Step title="Paste your API key">
Paste the key into the **API key** field. Sequel sends it to the Ahrefs API v3 as a Bearer token.
</Step>

<Step title="Test & save">
Click **Test**. Sequel validates the key against the free subscription-info endpoint (no units consumed). When it confirms, save the connection and it goes live.
</Step>
</Steps>

## What you can query

Sequel exposes read-only reports over the Ahrefs API v3:

- **Domain overview** — Domain Rating, organic/paid traffic, and keyword counts for a domain or URL.
- **Backlinks** — individual inbound links with anchor text and source metrics.
- **Referring domains** — unique linking domains and their authority.
- **Organic keywords** — keywords a target ranks for, with position, volume, and estimated traffic.
- **Top pages** — top pages by organic traffic and their lead keyword.
- **Keyword research** — search volume, difficulty, and CPC for a list of keywords via Keywords Explorer.

The connector is read-only; it cannot modify your Ahrefs projects or settings.

## Try it

- "What's the domain rating and organic traffic for ahrefs.com?"
- "Show the top 50 referring domains for competitor.com."
- "Which organic keywords does example.com rank for in the US top 10?"
- "What's the search volume and difficulty for 'text to sql'?"

## Troubleshooting

| Symptom | Likely fix |
| --- | --- |
| Test fails with an auth error | The API key is invalid or revoked. Create a fresh key in Ahrefs (**Account settings → API keys**) and paste it again. |
| Reports return no data or an access error | Your plan may lack API access or your API units are exhausted. Confirm an eligible paid plan and remaining units in Ahrefs. |
| "Domain not found" or empty results | Check the target domain/URL spelling and the country filter — Ahrefs may have no data for that market or target. |

## Related

- [Connecting your first data source](/docs/connecting-your-first-data-source)
- [Chatting with your data](/docs/chatting-with-your-data)
- [Browse all integrations](/docs/integrations)

---

# How to connect Google Ads to AI agents?

Connect Google Ads to Sequel with OAuth and ask about campaign spend, clicks, and conversions in natural language.

Connect **Google Ads** to Sequel and ask about your campaigns, spend, and conversions in plain English — then join paid-media performance with the revenue, signups, or pipeline data in your other sources. Under the hood Sequel queries Google Ads with GAQL (Google Ads Query Language), so you get campaigns, ad groups, ads, keywords, search terms, and metrics like impressions, clicks, cost, CTR, average CPC, and conversions — without writing a query yourself.

## What you'll need

- A **Google account** with access to the Google Ads account you want to query.
- At least **read access** to that Ads account.
- If you reach your accounts through a **manager (MCC) account**, the manager's **10-digit customer ID** (no dashes).

Google Ads connects over **OAuth**, so there's no key to copy — you authorize Sequel with your Google account. Sequel requests the `adwords` scope (read access to your Ads data).

<Callout>
Cost metrics come back in **micros** — 1,000,000 micros = one unit of the account's currency. Sequel divides this for you when you ask about spend, but it's worth knowing if you compare raw numbers against the Google Ads UI.
</Callout>

## Connect it

<Steps>
<Step title="Open a new connection">In the Sequel dashboard, go to **Connections → New connection** and choose **Google Ads**.</Step>
<Step title="Sign in with Google">You'll be redirected to Google's consent screen. Sign in and grant Sequel read access to your Google Ads data.</Step>
<Step title="Pick the Ads account">After authorizing, choose the Google Ads account (customer) you want to query. If that account sits under a **manager (MCC) account**, enter the manager's 10-digit customer ID so Sequel can reach it; leave it blank for a standalone account.</Step>
<Step title="Confirm">Sequel verifies access, lists the accounts it can see, and the connection goes live.</Step>
</Steps>

## Try it

- "How much did we spend on each campaign last month?"
- "Show daily clicks, cost, and conversions for our Search campaigns over the last 30 days."
- "Which campaigns have the best conversion value for the spend this quarter?"
- "List all enabled campaigns and their ad groups."

## Join it with other sources

> "For each Google Ads campaign last month, compare ad spend against the number of paid signups in our Postgres `users` table — which campaigns actually converted?"

## Troubleshooting

| Symptom | Likely fix |
| --- | --- |
| No accounts listed / "no accessible customers" | Confirm the Google account you signed in with has access to the Ads account. If you reach it through a manager account, set the **login customer ID** to the manager's 10-digit ID. |
| Authorization fails or data is denied | Re-run the connection and grant the **Ads read (`adwords`) scope** on Google's consent screen. Make sure you finish the redirect back to Sequel. |
| Empty results for a date range | Google Ads reporting has a short data lag — pick a date range that has ended (e.g. last week), and check the campaign was actually serving during that window. |

## Related

- [Google Analytics](/docs/google-analytics)
- [Google Search Console](/docs/google-search-console)
- [Chatting with your data](/docs/chatting-with-your-data)
- [Browse all integrations](/docs/integrations)

---

# How to connect Meta Ads to AI agents?

Connect Meta Ads (Facebook & Instagram) to Sequel with OAuth and ask about ad spend, ROAS, and campaign performance in plain English.

Connect **Meta Ads (Facebook & Instagram)** to Sequel and ask about ad spend, ROAS, impressions, clicks, and campaign performance in plain English — from the Sequel app or any AI tool wired up to the [MCP server](/docs/install). Then join it with your revenue or product data to answer questions a single tool can't.

## What you'll need

- A **Meta (Facebook) account** with access to the ad accounts you want to query.
- Authorization through Meta's OAuth flow — Sequel requests the **`ads_read`** and **`business_management`** scopes (read-only).

Meta Ads connects over **OAuth**, so there's no key to copy — you authorize Sequel with your Facebook account and it gets read access to the ad accounts that account can see.

<Callout type="info" title="Tokens last ~60 days">
Meta issues a long-lived token (~60 days) that can't be silently refreshed. When it nears expiry, Sequel asks you to reconnect so queries don't fail mid-run.
</Callout>

## Connect it

<Steps>
<Step title="Open a new connection">
In the Sequel dashboard, go to **Connections → New connection** and choose **Meta Ads**.
</Step>

<Step title="Sign in with Facebook">
You'll be redirected to Meta's consent screen. Sign in and grant Sequel read access (`ads_read`, `business_management`) to your advertising data.
</Step>

<Step title="Confirm">
Sequel verifies the connection (it reads your Meta profile to confirm access) and the connection goes live.
</Step>

<Step title="Pick an ad account when you query">
The connection covers every ad account your Facebook user can access. List them with **List Ad Accounts** to get each account id (in `act_<id>` form), then point your questions at the account you care about.
</Step>
</Steps>

## What you can query

- **Ad accounts** — id (`act_<id>`), name, status, currency, timezone, and business name.
- **Campaigns** — name, status, objective, schedule, and daily/lifetime budgets.
- **Ad sets** — status, budget, billing event, optimization goal, and targeting (geo, demographics, interests, audiences).
- **Ads** — name, status, parent ad set, and creative reference.
- **Insights** — spend, impressions, clicks, CTR, CPC, CPM, reach, frequency, and conversion actions (purchases and revenue live in `actions` / `action_values`) at account, campaign, ad set, or ad level — over presets like `last_30d` or an explicit date range, with daily/monthly buckets and breakdowns by age, gender, country, publisher platform, device, and more.

## Try it

- "What was our total Meta ad spend last 30 days, broken down by campaign?"
- "Which campaigns had the best ROAS this month?"
- "Show daily spend and clicks for the last 7 days."
- "Break down impressions by country and publisher platform for last month."

## Join it with other sources

Meta Ads gets more powerful next to your other data. With a database also connected, ask:

> "Compare last month's Meta ad spend by campaign against signups in our Postgres `users` table over the same dates."

## Troubleshooting

| Symptom | Likely fix |
| --- | --- |
| No ad accounts listed | The Facebook account you authorized doesn't have access to any ad account — reconnect with an account that does, and make sure you granted the `ads_read` / `business_management` scopes. |
| Authorization fails | Re-run the OAuth flow and approve the requested read scopes; if it persists, reconnect to issue a fresh 60-day token. |
| Insights come back empty | Confirm the ad account actually ran delivery in the date range, and that you're querying the right `object_id` and `level`. |

## Related

- [Connecting your first data source](/docs/connecting-your-first-data-source)
- [Chatting with your data](/docs/chatting-with-your-data)
- [Browse all integrations](/docs/integrations)

---

# How to connect HubSpot to AI agents?

Connect HubSpot to Sequel with OAuth and ask about contacts, companies, deals, tickets, and pipelines in natural language.

Connect **HubSpot** to Sequel and ask about your CRM — contacts, companies, deals, tickets, and pipeline — in plain English, from the Sequel app or any AI tool wired up to the [MCP server](/docs/install). Then join it with your product or revenue data for questions a single tool can't answer alone.

## What you'll need

- A **HubSpot account** (portal) with access to the CRM data you want to query.
- Nothing to copy or paste — HubSpot connects over **OAuth**. You authorize Sequel and pick the portal.

Sequel requests **read-only** scopes for CRM objects and their schemas: `crm.objects.{contacts,companies,deals,tickets}.read` and `crm.schemas.{contacts,companies,deals,tickets}.read`. No write scopes are requested, so Sequel cannot create, update, or delete records.

## Connect it

<Steps>
<Step title="Open a new connection">
In the Sequel dashboard, go to **Connections → New connection**, choose **HubSpot**, and give it a name.
</Step>

<Step title="Connect with HubSpot">
Click **Connect with HubSpot**. You'll be redirected to HubSpot's OAuth screen.
</Step>

<Step title="Pick a portal and authorize">
Choose the HubSpot portal you want Sequel to query and confirm the read-only CRM scopes. Connect additional portals later by repeating this flow — each portal is a separate Sequel data source.
</Step>

<Step title="Confirm">
Sequel verifies the token against your hub and the connection goes live.
</Step>
</Steps>

<Callout type="info" title="Custom properties just work">
The connector reads each object's property schema at query time, so your real custom property names (for example `ACV` or `Onboarding Status`) show up in answers without any manual setup.
</Callout>

## What you can query

Sequel exposes read tools over the HubSpot v3 CRM API:

- **Contacts** — search by any property combination, custom or default.
- **Companies** — segment by industry, lifecycle stage, or custom fields.
- **Deals** — filter and aggregate by stage, owner, pipeline, amount, or close date.
- **Tickets** — volume, priority, stage, and aging across pipelines.
- **Pipelines** and **properties** — enumerate deal/ticket pipeline + stage IDs and discover available property names per object type.

## Try it

- "How many open deals do we have, broken down by stage and owner?"
- "What's the win rate by lead source for deals closed this quarter?"
- "Show me deals over $50k that have been in 'Proposal Sent' for more than 30 days."
- "List the 20 most recently created tickets with their priority and pipeline."

## Join it with other sources

HubSpot gets more powerful alongside your other data. With a database also connected, ask:

> "For accounts in the 'Closed Won' stage in HubSpot, what's their current MRR and active seat count in our Postgres `subscriptions` table?"

## Troubleshooting

| Symptom | Likely fix |
| --- | --- |
| Authorization fails | Re-run the OAuth flow and confirm you grant the requested read-only CRM scopes. Tokens refresh automatically, but a revoked app needs reconnecting. |
| Missing object or property | The authorized portal may lack access to that CRM object, or the user who connected doesn't have permission — reconnect with an account that can see the data. |
| Data looks empty | Confirm the portal actually has records matching the filters and date range you're asking about. |

## Related

- [Connecting your first data source](/docs/connecting-your-first-data-source)
- [Chatting with your data](/docs/chatting-with-your-data)
- [Browse all integrations](/docs/integrations)

---

# How to connect Apollo to AI agents?

Connect Apollo.io to Sequel with an API key and ask about your prospects, contacts, and accounts in natural language.

Connect **Apollo.io** to Sequel and ask about your prospecting and sales data in plain English — search Apollo's B2B database of people and companies, and slice the contacts and accounts your team has saved in Apollo — from the Sequel app or any AI tool wired up to the [MCP server](/docs/install). Apollo connects with a single API key, and Sequel only ever reads.

## What you'll need

- An **Apollo.io account** with access to **Settings → Integrations → API**.
- An **Apollo master API key**. The People and Organization search endpoints require a **master** key (not a scoped key), so create the key with master access enabled.

<Callout type="info" title="Searching Apollo's database consumes credits">
The **Search People** and **Search Organizations** tools query Apollo's global prospect/company database and consume Apollo credits per their pricing. The **Search Contacts** and **Search Accounts** tools read your own saved CRM data and consume no credits.
</Callout>

## Connect it

<Steps>
<Step title="Open a new connection">
In the Sequel dashboard, go to **Connections → New connection** and choose **Apollo.io**.
</Step>

<Step title="Create a master API key in Apollo">
In Apollo, go to **Settings → Integrations → API → Create New Key** and enable **master** access. The key is passed in the `X-Api-Key` header on every request.
</Step>

<Step title="Paste the key into Sequel">
Copy the key from Apollo and paste it into the **API key** field on Sequel's Apollo connector.
</Step>

<Step title="Test and save">
Sequel verifies the key against the Apollo API. A green check means it's live.
</Step>
</Steps>

## What you can query

Sequel exposes Apollo as four read-only search tools:

- **People** — search Apollo's global prospect database by job title, seniority, location, employer, employee-count band, and keywords. (Consumes credits; does **not** return email addresses or phone numbers.)
- **Organizations** — search Apollo's global company database by location, headcount range, industry keyword tags, and domains. (Consumes credits.)
- **Contacts** — search the contacts your team has saved in Apollo (your CRM). No credits consumed.
- **Accounts** — search the accounts (companies) your team has saved in Apollo. No credits consumed.

<Callout type="info" title="Read-only">
The connector ships search tools only — it can't create, update, or delete any Apollo record.
</Callout>

## Try it

- "Find VPs of Sales at SaaS companies in the US with 50-200 employees."
- "Which of our Apollo contacts were added in the last 30 days?"
- "List our accounts in the 'Negotiation' stage, sorted by last activity."
- "Show me companies in the mining industry headquartered in Canada."

## Join it with other sources

Apollo is most useful next to your other data. With a database also connected, ask:

> "Match our Apollo accounts against the customer table in Postgres, and show which prospects aren't customers yet."

## Troubleshooting

| Symptom | Likely fix |
| --- | --- |
| Apollo rejected the API key (`is_logged_in=false`) | Re-check the key for typos, or create a fresh key in **Settings → Integrations → API** and paste it again. |
| People/Organization search errors or returns nothing | The key likely lacks **master** access — those endpoints require it. Recreate the key with master access enabled. |
| A search returns no results | Loosen your filters and confirm there's data in range. For "our" contacts/accounts use the CRM tools; for net-new prospecting use the global search tools. |

## Related

- [Connecting your first data source](/docs/connecting-your-first-data-source)
- [Chatting with your data](/docs/chatting-with-your-data)
- [Browse all integrations](/docs/integrations)

---

# How to connect Intercom to AI agents?

Connect Intercom to Sequel with OAuth and ask about conversations, support volume, response times, and customers in plain English.

Connect **Intercom** to Sequel and ask about your support conversations, tickets, response times, and customers in plain English — from the Sequel app or any MCP/AI client wired up to the [MCP server](/docs/install). Then join it with your product or revenue data for questions a single tool can't answer alone.

## What you'll need

- An **Intercom account** with access to the workspace you want to query.
- Permission to authorize an OAuth app for that workspace (typically a workspace admin).

Intercom connects over **OAuth**, so there's no key to copy or paste — you authorize Sequel with your Intercom account and the workspace is detected automatically. The tokens Sequel receives are **read-only** and don't expire.

## Connect it

<Steps>
<Step title="Open a new connection">
In the Sequel dashboard, go to **Connections → New connection** and choose **Intercom**.
</Step>

<Step title="Connect with Intercom">
You'll be redirected to Intercom's authorization screen (`app.intercom.com/oauth`). Sign in and approve access for the workspace you want Sequel to query.
</Step>

<Step title="Confirm">
Sequel exchanges the code, reads your workspace from Intercom's `/me` endpoint, and names the connection after it (e.g. *Intercom — Acme Support*). The connection goes live once access is verified.
</Step>
</Steps>

<Callout type="info" title="One workspace per connection">
Each connection maps to a single Intercom workspace. To query another workspace, repeat the flow and authorize that one.
</Callout>

## What you can query

Sequel exposes read access to your Intercom workspace through these tools:

- **Conversations** — search by open/closed state, assignee, team, tags, priority, source, and reply-time statistics.
- **Tickets** — filter by ticket state, category, type, and assignee.
- **Contacts** — users and leads, by email, role, signup/last-seen dates, or any custom attribute.
- **Companies** — full export with plan, spend, session and user counts, size, and industry.
- **Tags, Teams, and Admins** — small lookups, handy for resolving names to ids before a search.

## Try it

- "How many conversations were opened last week, and how many are still open?"
- "What's our median time to first reply this month, by team?"
- "List contacts that signed up in the last 30 days but haven't been seen since."
- "Which companies have the highest monthly spend?"

<Callout type="info" title="Timestamps are Unix seconds">
Intercom returns all dates as Unix seconds. Just ask in plain language — Sequel handles the conversion.
</Callout>

## Join it with other sources

Intercom gets more powerful alongside your other data. With a database also connected, ask:

> "For accounts with an open Intercom ticket this week, what's their MRR in our Postgres `subscriptions` table?"

## Troubleshooting

| Symptom | Likely fix |
| --- | --- |
| Authorization fails | Re-run the OAuth flow and approve access for the correct workspace; make sure your Intercom user can authorize apps. |
| Wrong workspace connected | You authorized a different workspace — start a new connection and pick the right one on Intercom's screen. |
| "Rate limit" errors on large pulls | Intercom caps search at 150 rows per page; narrow the date range or let Sequel page through with the cursor. |

## Related

- [Connecting your first data source](/docs/connecting-your-first-data-source)
- [Chatting with your data](/docs/chatting-with-your-data)
- [Browse all integrations](/docs/integrations)

---

# How to connect Stripe to AI agents?

Connect Stripe to Sequel with a read-only API key and ask about MRR, churn, refunds, and payouts in natural language.

Connect **Stripe** to Sequel and ask about your payments, revenue, and subscriptions in plain English — MRR, churn, refunds, disputes, payouts, and more — from the Sequel app or any AI tool wired up to the [MCP server](/docs/install). Stripe connects with a single API key, and Sequel only ever reads.

## What you'll need

- A **Stripe account** with access to the Dashboard.
- A **Stripe API key** — preferably a **Restricted Key** (starts with `rk_`) scoped **read-only** to the resources you want Sequel to query. A standard secret key (`sk_`) also works, but a restricted key is safer.

<Callout type="tip" title="Use a restricted, read-only key">
The Stripe connector ships only read tools (list, get, search). Pairing it with a Restricted Key scoped to read-only access means Sequel literally can't modify your account, even in principle. Scope the key to just the resources you care about (customers, charges, invoices, subscriptions, etc.), and revoke or rotate it from the Stripe Dashboard at any time without touching your other integrations.
</Callout>

## Connect it

<Steps>
<Step title="Open a new connection">
In the Sequel dashboard, go to **Connections → New connection** and choose **Stripe**.
</Step>

<Step title="Create a Restricted Key in Stripe">
In your Stripe Dashboard, go to **Developers → API keys → Create restricted key**. Set the resources you want Sequel to read (Customers, Charges, Invoices, Subscriptions, Products, Prices, Refunds, Disputes, Payouts, Balance) to **Read**, leave everything else **None**, and create the key. Use a `rk_test_…` key for test-mode data or `rk_live_…` for live data.
</Step>

<Step title="Paste the key into Sequel">
Copy the key from Stripe and paste it into the **API key** field on Sequel's Stripe connector.
</Step>

<Step title="Test and save">
Sequel verifies the key against the Stripe API. A green check means it's live.
</Step>
</Steps>

<Callout type="info" title="Test and live data stay separate">
Want both? Create two connections — one with a `rk_test_…` key and one with `rk_live_…`. Sequel keeps them isolated, and the connection name makes it obvious which is which.
</Callout>

## What you can query

Sequel exposes Stripe's core objects as read-only tools:

- **Customers** — find by email, metadata, or creation date.
- **Charges** and **Payment Intents** — including failed payments and their reason codes.
- **Invoices** and **Subscriptions** — by plan, price, status, or customer.
- **Products** and **Prices** — your catalog and pricing.
- **Refunds** and **Disputes** — over any period, by status.
- **Payouts** and **Balance Transactions** — what hit your bank and the fees behind it.
- **Balance** — current available and pending funds per currency.
- **Search** — full-text queries using Stripe Search Query Language (e.g. `email:"foo@bar.com"`, `status:"active" AND created>1700000000`) across customers, charges, invoices, subscriptions, products, prices, and payment intents.

## Try it

- "What's our MRR for May, broken down by plan?"
- "How many customers churned last month?"
- "List failed payment intents in the last 7 days with the failure reason."
- "What's our current available balance per currency?"

## Join it with other sources

Stripe is most powerful next to your product data. With a database also connected, ask:

> "For customers who churned in Stripe last month, what features did they use in our Postgres `events` table?"

## Troubleshooting

| Symptom | Likely fix |
| --- | --- |
| Invalid or expired key | Re-check the key for typos, or create a fresh Restricted Key in the Stripe Dashboard and paste it again. |
| A resource returns nothing or errors | The Restricted Key is missing **Read** permission on that resource — edit the key in Stripe and grant read access. |
| Data looks empty | Confirm there's actually data in the date range you're asking about, and that you used a live-mode key (`rk_live_…`) if you expect live data. |

## Related

- [Connecting your first data source](/docs/connecting-your-first-data-source)
- [Chatting with your data](/docs/chatting-with-your-data)
- [Browse all integrations](/docs/integrations)

---

# How to connect Polar to AI agents?

Connect Polar to Sequel with a read-only Organization Access Token and ask about MRR, churn, subscriptions, and usage billing in natural language.

Connect **Polar** to Sequel and ask about your revenue, subscriptions, customers, and usage-based billing in plain English — MRR, ARR, churn rate, LTV, failed payments, meter usage — from the Sequel app or any AI tool wired up to the [MCP server](/docs/install). Polar connects with a single access token, and Sequel only ever reads.

## What you'll need

- A **Polar organization** you can access the settings for.
- A **Polar Organization Access Token** (starts with `polar_oat_`) scoped to **read-only** permissions.

<Callout type="tip" title="Grant read scopes only">
The Polar connector ships only read tools — it cannot create, refund, or cancel anything. Granting your token only the `:read` scopes enforces that at Polar's end too, so the integration is safe even in principle. You can revoke or rotate the token in Polar at any time without touching your other integrations.
</Callout>

## Connect it

<Steps>
<Step title="Open a new connection">
In the Sequel dashboard, go to **Connections → New connection** and choose **Polar**.
</Step>

<Step title="Create an access token in Polar">
In Polar, go to **Organization Settings → Developers → New Token**. Give it a name, pick an expiry, and enable these read scopes:

`metrics:read`, `orders:read`, `subscriptions:read`, `customers:read`, `products:read`, `payments:read`, `refunds:read`, `disputes:read`, `discounts:read`, `checkouts:read`, `events:read`, `meters:read`, `customer_meters:read`, `organizations:read`

Leave every `:write` scope off.
</Step>

<Step title="Paste the token into Sequel">
Copy the token — Polar shows it only once — and paste it into the **API key** field on Sequel's Polar connector.
</Step>

<Step title="Choose the environment">
Leave **Server** blank (or set it to `production`) for your live organization. Enter `sandbox` if the token came from a Polar sandbox organization.
</Step>

<Step title="Test and save">
Sequel verifies the token against the Polar API and names the connection after your organization. A green check means it's live.
</Step>
</Steps>

<Callout type="info" title="Sandbox and production are separate accounts">
Polar's sandbox is a fully isolated deployment with its own organization and its own tokens — a sandbox token will not authenticate against production. If you want both, create two connections and set the environment on each.
</Callout>

## What you can query

Sequel exposes Polar as 14 read-only tools:

- **Metrics** — Polar's 55 pre-computed metrics as a time series with totals: MRR, ARR, committed MRR, churn rate, LTV, ARPU, revenue and net revenue, cashflow, average order value, gross margin, checkout conversion, per-reason cancellations, and seat metrics.
- **Orders** — individual purchases and renewals, filterable by status, date range, product, customer, discount, or subscription.
- **Subscriptions** — status, amount, trial windows, period boundaries, cancellation timestamps and reasons.
- **Customers** and **Customer state** — the customer list, plus a single call returning one customer's active subscriptions, granted benefits, and active meters.
- **Products** — your catalogue, with prices nested inside each product.
- **Payments**, **Refunds**, and **Disputes** — including failed payment attempts and their decline reasons.
- **Discounts** and **Checkouts** — code redemptions and checkout-funnel drop-off.
- **Meters**, **Meter quantities**, and **Events** — usage-based billing: what you meter, how much was consumed over time, and the raw ingested events behind it.

<Callout type="info" title="Amounts are in cents">
Polar returns every monetary value as an integer number of cents. Sequel leaves them that way so sums stay exact, and the metrics result includes a `units` row telling you which columns are currency, which are counts, and which are percentages.
</Callout>

## Try it

- "What's our MRR trend over the last six months?"
- "Which cancellation reason drove the most churn last quarter?"
- "How many payments failed last month, and for which customers?"
- "What's our checkout conversion rate by product?"

## Join it with other sources

Polar is most powerful next to your product data. With a database or product analytics tool also connected, ask:

> "For customers who churned in Polar last month, what features did they use in our Postgres `events` table?"

## Troubleshooting

| Symptom | Likely fix |
| --- | --- |
| Invalid or expired token | Re-check the token for typos, or mint a fresh Organization Access Token in Polar and paste it again. Tokens expire on the schedule you set when creating them. |
| A tool returns a permissions error | The token is missing the `:read` scope for that resource. Edit the token's scopes in Polar, or create a new one with the full read set above. |
| Authentication fails against the right-looking token | Check the environment. A `sandbox` token will not work against production, and vice versa. |
| A metrics question returns a validation error | The interval is too fine for the date range. Widen `interval` from `hour` to `day`, or from `day` to `month`, and ask again. |
| Data looks empty | Confirm there's data in the period you're asking about, and that the token belongs to the organization you expect — a token is bound to exactly one organization. |

## Limitations

Polar's public API has **no payouts or balance endpoints**, so questions about how much Polar has paid out, payout schedules, or your current balance can't be answered by any integration, Sequel included. Revenue questions work fine — just note that revenue is not the same as what reaches your bank account, since it's before Polar's fees, taxes remitted, and payout timing.

## Related

- [Connecting your first data source](/docs/connecting-your-first-data-source)
- [Chatting with your data](/docs/chatting-with-your-data)
- [Browse all integrations](/docs/integrations)

---

# How to connect Dodo Payments to AI agents?

Connect Dodo Payments to Sequel with a read-only API key and ask about revenue, subscriptions, failed payments, refunds, disputes and payouts in natural language.

Connect **Dodo Payments** to Sequel and ask about your revenue, subscriptions, customers, refunds, disputes and payouts in plain English, from the Sequel app or any AI tool wired up to the [MCP server](/docs/install). Dodo connects with a single API key, and Sequel only ever reads.

## What you'll need

- A **Dodo Payments business** you can access the dashboard for.
- A **Dodo Payments API key** created with write access turned off.

<Callout type="tip" title="Create a read-only key">
When you add a key in Dodo, leave **Enable write access** unchecked. The key can then fetch payments, subscriptions, customers and products but cannot create or modify anything. The Sequel connector ships only read tools, so a read-only key enforces at Dodo's end what Sequel already guarantees. You can revoke the key in Dodo at any time.
</Callout>

## Connect it

<Steps>
<Step title="Open a new connection">
In the Sequel dashboard, go to **Connections → New connection** and choose **Dodo Payments**.
</Step>

<Step title="Create an API key in Dodo">
In the Dodo Payments dashboard, go to **Developer → API Keys → Add API Key**. Give it a name and leave **Enable write access** unchecked.
</Step>

<Step title="Paste the key into Sequel">
Copy the key (Dodo shows it once) and paste it into the **API key** field on Sequel's Dodo Payments connector.
</Step>

<Step title="Choose the environment">
Leave **Environment** on `live` for your real account. Choose `test` if the key came from Dodo's test mode.
</Step>

<Step title="Test and save">
Sequel verifies the key against the Dodo API and names the connection after your brand. A green check means it's live.
</Step>
</Steps>

<Callout type="info" title="Test mode and live mode are separate">
Dodo's test mode runs at `test.dodopayments.com` with its own keys and data. A test key will not authenticate against live, and vice versa. To query both, create two connections with the environment set on each.
</Callout>

## What you can query

Sequel exposes Dodo Payments as 10 read-only tools:

- **Payments**: every charge attempt, including failed ones, with status, amount, currency, customer, product, subscription, payment method and card details. Filter by status, date range, customer, product, subscription, brand or currency.
- **Payment detail**: one payment in full: the product cart, settlement amount and currency, tax, discount, and the refunds and disputes attached to it, plus the error code when it failed.
- **Subscriptions**: status (`active`, `on_hold`, `paused`, `cancelled`, `failed`, `expired`, `pending`), recurring amount, billing interval, next and previous billing dates, trial days, and whether the subscription is set to cancel at the next billing date.
- **Customers**: the customer list, filterable by email, name or created date.
- **Products**: the catalogue with price, recurring or one-time, tax category and entitlements.
- **Refunds** and **Disputes**: refund status, partial vs full and reason; chargebacks by stage and status.
- **Payouts**: each payout to your bank with fee, refunds, chargebacks and tax already netted out.
- **Discounts**: codes, type, times used, limits and expiry.
- **Balance ledger**: every payment, fee, refund, dispute, payout and adjustment as a signed entry with before and after balances, filterable by event type and currency.

<Callout type="info" title="Amounts are in the currency's smallest unit">
Dodo returns every monetary value as an integer in the currency's smallest unit: cents for USD and EUR, yen for JPY. Sequel's agent knows this and divides before reporting, and it never sums across currencies without saying so. Ledger entries also carry a USD-equivalent amount for cross-currency totals.
</Callout>

## Try it

- "What was our revenue last month, by currency?"
- "How many subscriptions are on hold, and which customers are they?"
- "Show payments that failed this week with their error codes."
- "How much has been paid out this year, net of fees?"

## Join it with other sources

Dodo is most useful next to the rest of your stack. With Search Console and GA4 also connected, ask:

> "Which organic search landing pages brought in the customers who paid in Dodo last quarter?"

Sequel's [revenue playbooks](/skills) already know about Dodo, so a question like that follows a tested procedure rather than an improvised join.

## Troubleshooting

| Symptom | Likely fix |
| --- | --- |
| Authentication fails (401) | Re-check the key for typos, and check the environment: a key created in test mode only works with **Environment** set to `test`. |
| A subscription question returns nothing | Check the status filter. Dodo has no `trialing` status; new subscriptions start as `pending`, and a failed renewal shows as `on_hold`. |
| Amounts look 100x too large | They are in the smallest currency unit. Divide by 100 for USD, EUR and most currencies; JPY and a few others have no decimals. |
| Rate limit (429) | Dodo allows 40 requests per second and 240 per minute on the default tier. Sequel surfaces the reset time; wait and retry. |
| Data looks empty | Confirm there's data in the period and environment you're asking about; a key is bound to one business and one environment. |

## Limitations

Dodo's list endpoints do not filter on metadata. If you store attribution (UTM or referrer values) in payment metadata, Sequel pulls the window and filters the metadata column in the workbench rather than at the API. Usage-based billing meters and license keys are not exposed by this connector yet.

## Related

- [Connecting your first data source](/docs/connecting-your-first-data-source)
- [Chatting with your data](/docs/chatting-with-your-data)
- [Browse all integrations](/docs/integrations)

---

# How to connect Google Sheets to AI agents?

Connect Google Sheets to Sequel with OAuth and ask questions about your spreadsheet data in plain English.

Connect **Google Sheets** to Sequel and ask questions about the data in your spreadsheets in plain English — from the Sequel app or any AI tool wired up to the [MCP server](/docs/install). Once a spreadsheet is connected, you can read any tab and join that data with your databases for questions a single tool can't answer alone.

## What you'll need

- A **Google account** with access to the spreadsheet you want to query (read access is enough).
- Nothing to copy or configure — Google Sheets connects over **OAuth**. After you sign in, you pick the spreadsheet from a Google file picker; there's no URL or ID to paste.

<Callout type="info" title="Scoped to the sheet you pick">
Sequel requests Google's `drive.file` scope, which grants access only to the specific spreadsheet you select in the picker. It cannot read, list, or modify any other files in your Google Drive.
</Callout>

## Connect it

<Steps>
<Step title="Open a new connection">
In the Sequel dashboard, go to **Connections → New connection** and choose **Google Sheets**.
</Step>

<Step title="Sign in with Google">
You'll be redirected to Google's consent screen. Sign in and grant Sequel access to the spreadsheet you'll choose next.
</Step>

<Step title="Pick a spreadsheet">
Google's file picker opens, filtered to spreadsheets. Select the one you want Sequel to query.
</Step>

<Step title="Confirm">
Sequel verifies it can read the spreadsheet, indexes its tabs and column headers, and the connection goes live. One connection represents one spreadsheet — repeat this flow to add more.
</Step>
</Steps>

## What you can query

Sequel exposes the connected spreadsheet as queryable tabs:

- **List the sheet tabs** in the spreadsheet, with each tab's row and column counts.
- **Read rows from any tab** — optionally narrowed to an A1 range like `A1:Z100`. The first row of each tab is treated as the column headers, so the AI can filter and reference columns by name.

On connect, Sequel reads the header row of each tab to learn the spreadsheet's structure, which lets it route your questions to the right tab and columns automatically.

## Try it

- "What sheets are in this spreadsheet?"
- "Show me the first 50 rows from the Sales sheet"
- "What are the column headers in the Inventory sheet?"
- "How many rows are in the Orders sheet?"

## Join it with other sources

Spreadsheet data gets more powerful alongside your databases. With a database also connected, ask:

> "Reconcile the headcount plan in my Planning sheet against the actual `employees` table in Postgres — which roles in the plan don't have a matching active employee?"

## Troubleshooting

| Symptom | Likely fix |
| --- | --- |
| "Access denied" on connect | The Google account you authorized doesn't have access to that spreadsheet — sign in with an account that can open it, or have the owner share it with you. |
| Authorization fails | Re-run the OAuth flow and make sure you grant the requested access on Google's consent screen. |
| "Spreadsheet not found" or a tab returns nothing | Pick the spreadsheet again from the file picker, and check you're naming the exact tab — sheet names are case-sensitive. |

## Related

- [Connecting your first data source](/docs/connecting-your-first-data-source)
- [Chatting with your data](/docs/chatting-with-your-data)
- [Browse all integrations](/docs/integrations)

---

# Introduction

What Sequel is — the data layer for your agents. It manages auth, credentials, and execution to securely connect your cloud databases, warehouses, and SaaS tools to the AI agents you already use, over MCP.

Sequel is the **data layer for your agents**. You connect your cloud databases, warehouses, and SaaS tools to Sequel once, and then the AI tools you already use — Claude, Cursor, ChatGPT, or any other MCP-capable agent — can ask questions of your data in plain English and get back real answers. Your agents talk to Sequel, and Sequel safely talks to your data. No more handing out database passwords or API keys to every tool.

![Sequel sits between your data stack and your AI agents](/docs/data-layer.png)

## Why use Sequel

- **One secure boundary.** Your credentials live in Sequel, encrypted. Agents ask Sequel to run work — they never see the secret behind a connection. Authorize a tool once, revoke it any time.
- **Connect once, use everywhere.** The same connections power every MCP client and the CLI. Add a new agent with a permission grant, not another round of credential setup.
- **Answers across sources.** Most real questions span more than one system. Sequel can join your Postgres revenue, Google Analytics sessions, and Stripe payments in a single thread.
- **Real analysis, not just queries.** A built-in Python sandbox (pandas, matplotlib, and more) lets agents compute, chart, and export results — backed by your actual data.

## What Sequel manages for you

Sitting between your agents and your data, Sequel owns the three things that make agent access hard to get right:

| Concern | How Sequel handles it |
| --- | --- |
| **Auth** | Every agent and MCP client connects through Sequel's OAuth flow and scoped API keys. You authorize a tool once, and you can revoke that access at any time without touching the data source itself. |
| **Credentials** | Database passwords, connection strings, and SaaS API keys are stored and encrypted on Sequel's side. Agents never see them — they ask Sequel to run work on a connection, not for the secret behind it. |
| **Execution** | Queries and Python analyses run in Sequel's controlled, sandboxed environment, then return clean results. Your agents get answers and charts back; your data sources only ever talk to Sequel. |

This is what "data layer" means in practice: one secure boundary in front of all your sources, so onboarding a new agent is a permission grant rather than a secret-sharing exercise.

## What you can do with it

- **Ask questions across sources.** Join Postgres revenue with Google Analytics sessions and Stripe payments in a single thread.
- **Explore schemas in natural language.** "What tables relate to subscriptions, and how?"
- **Run real analysis.** Sequel includes a Python sandbox (pandas, matplotlib, and more) so agents can compute, chart, and export results.
- **Connect once, use everywhere.** The same connections power every MCP client and the CLI — no per-tool credential setup.

## Who it's for

- **Data and analytics teams** who want self-serve answers without writing every query by hand.
- **Engineers** who want schema-aware, data-grounded answers inside their editor or CLI.
- **Operators and PMs** who live in ChatGPT or Claude and want those tools to answer with real numbers instead of guesses.

<Callout type="info" title="Sequel connects to cloud-hosted data sources">
Managed Postgres and MySQL (RDS, Supabase, Neon, PlanetScale, and similar), warehouses like BigQuery and ClickHouse, and SaaS APIs like Stripe and Google Analytics.
</Callout>

## Next steps

- **[Getting Started](/docs/getting-started)** — connect the MCP server and run your first query.
- **[Install Sequel MCP](/docs/install)** — copy-paste setup for every supported AI tool.
- **[Integrations](/docs/integrations)** — browse every supported data source and its setup guide.

---

# Sequel Skills

> Playbooks an agent follows step by step against the user's connected accounts. Each names the exact `app.tool` calls, in order, with the gotchas. Agents read them at runtime with `sequel_read_skill` (by slug); the browsable directory is https://sequel.sh/skills.

---

## Skill: SEO: audit, diagnose and improve organic search

- Slug: `seo`
- Category: SEO
- Works with: google_search_console, google_analytics, ahrefs
- Page: https://sequel.sh/skills/seo

Start here for any SEO or organic search question, including vague ones: something feels off with our Google numbers, how is our site doing in search, audit our SEO, give me an honest summary of SEO for leadership, why did traffic or rankings drop, what should we fix or write next. Covers Search Console and GA4 organic data, keywords and rankings, CTR, cannibalization, content decay, competitors and backlinks, keyword gaps, indexing and sitemaps, technical audits, redirects, metadata, internal links, AI crawlers, llms.txt and AI search visibility, and which organic pages convert or earn revenue. Connects the right accounts, routes to the focused playbook, and sets the evidence rules every SEO answer follows.

Every SEO question starts here. This playbook does three things: makes sure the right accounts are connected, routes you to the focused playbook for the job, and sets the evidence rules that every SEO answer from Sequel follows.

## Connect first

SEO work runs on the user's own accounts, not on generic advice. The `apps` list in this tool result says which are connected.

- **Google Search Console is required** for nearly everything. It is the only measured source of the site's own queries, clicks, impressions and positions.
- **Google Analytics 4** turns traffic into outcomes: engagement, conversions, revenue. Needed for any question about whether the traffic matters.
- **Ahrefs** adds competitors, backlinks and keyword estimates. Optional. Without it, skip the competitor playbooks rather than guessing.

If a required app shows `connected: false`, call `sequel_manage_connections` with `action: "connect"` and that `app_id`, then give the user the link it returns. Never ask for credentials, tokens, property IDs or site URLs in chat; the hosted flow collects everything. Do not stop the investigation while waiting: run every step the connected sources support and say plainly which steps were skipped and why. After the user reports they have connected, call `sequel_manage_connections` with `action: "list"` to confirm before continuing. There is no automatic notification.

Each connection is pinned to one account or property (a Search Console site, a GA4 property, an Ahrefs account). `sequel_read_skill` lists them by name under `apps[].connections`. If an app has more than one and the question does not name the site, ask which to use before running anything, and never combine two properties in one answer.

If the user names a tool Sequel does not connect to (Semrush, Moz, Bing Webmaster Tools, Screaming Frog, Google Ads and others), say so in one sentence before doing anything else, then offer the same question answered from the sources Sequel does have. Do not substitute silently, and do not present Search Console or Ahrefs numbers as if they came from the tool the user asked about.

## Route to the right playbook

Read the matching playbook with `sequel_read_skill`, passing its slug, then follow it. Run one playbook at a time; its output usually names the next.

| The user wants to know | Read |
| --- | --- |
| Something feels off, not sure what is wrong | `traffic-drop-investigation` |
| Why organic traffic or clicks dropped, and when | `traffic-drop-investigation` |
| Which queries fell off page one | `page-one-losses` |
| Which queries are new, and which disappeared | `new-and-lost-queries` |
| Which keywords are close to page one and worth pushing | `find-striking-distance-keywords` |
| Which page-one rankings get too few clicks | `ctr-underperformers` |
| Which pages are slowly losing traffic and need a refresh | `content-decay-audit` |
| Whether two pages compete for the same query | `keyword-cannibalization-check` |
| Which pages win in search, and what they rank for | `analyze-high-ranking-landing-pages` |
| How much traffic is brand vs non-brand | `brand-vs-nonbrand-split` |
| What to write next, based on real demand | `query-clusters-to-content-plan` |
| Which keywords competitors rank for that we do not | `competitor-keyword-gap` |
| Which competitor pages earn the most, and why | `competitor-top-pages-teardown` |
| Whether the backlink profile is healthy | `backlink-profile-health-check` |
| Which referring domains are worth anything | `referring-domain-quality-audit` |
| Which backlinks were lost and can be recovered | `lost-backlink-recovery` |
| Which sites are linking to competitors right now, and how they earn links | `competitor-new-backlinks` |
| Whether organic traffic converts, page by page | `organic-landing-page-conversion` |
| Which organic pages and queries produce revenue or pipeline | `organic-traffic-to-revenue` |
| How SEO is going overall, for a leadership or board update | `organic-traffic-to-revenue`, then `brand-vs-nonbrand-split` for the trend |
| Whether paid search is buying clicks that organic already earns | `paid-and-organic-overlap` |
| Why a specific page is not indexed | `page-not-indexed-debug` |
| Whether the pages that matter are actually indexed | `index-coverage-review` |
| Whether the sitemaps are healthy and complete | `sitemap-health` |
| Whether the site has technical problems (status, canonicals, noindex) | `technical-crawl-audit` |
| Whether redirects chain, loop or leak traffic | `redirect-chain-audit` |
| Whether titles and descriptions match what pages rank for | `metadata-audit` |
| Whether important pages get enough internal links | `internal-link-audit` |
| Whether AI crawlers (GPTBot, ClaudeBot, PerplexityBot) can reach the site | `ai-crawler-access-check` |
| Whether the site has a useful llms.txt, or needs one drafted | `llms-txt-audit-and-draft` |
| How much traffic AI assistants send, and to which pages | `ai-referral-traffic` |

A broad request like "audit our SEO" has no natural stopping point. Start with `traffic-drop-investigation` if performance is the concern, or `technical-crawl-audit` if the site itself is, then let that result narrow the next call. Do not run every playbook.

## Evidence rules

These apply to every playbook. State them in the answer where they bound a conclusion.

- **Search Console lags two to three days.** Never end a date window at today. End it three days back, and say so, or the last days will read as a fake cliff.
- **Search Console truncates and anonymises.** Rows are capped, low-volume queries are omitted for privacy, and grouped totals undercount. Per-query rows never sum to the page total. Do not present the gap as missing traffic.
- **Position is an average across impressions.** A page "at 8.4" may rank third for its main term and fortieth for a long tail. Look at per-query rows before acting on it.
- **Join Search Console to GA4 on the URL path.** Search Console returns absolute URLs; GA4's `landingPage` is a path. Strip the origin, trailing slash and query string from both before joining.
- **Measured beats modelled.** Search Console and GA4 are measured. Ahrefs volume, difficulty, traffic and value are estimates. Quote measured numbers for the site's own performance, use estimates for competitors and market size, and never average the two.
- **Strip brand queries before ranking opportunities.** They already convert and cannot be improved much. Ask for the brand terms if they are not obvious.
- **Missing is not zero.** A skipped step, an unconnected source or a capped result bounds the conclusion; it does not fill the gap.
- **Page content comes from your own browsing.** Sequel supplies account data. When a playbook needs a live page, robots.txt or a sitemap, fetch it with your own web tools. If you cannot browse, run the account-data steps and say which checks you skipped.
- **Never promise rankings, clicks, indexing or AI citations.** Report what the evidence supports and what to verify after a change.

---

## Skill: Analyze high-ranking landing pages

- Slug: `analyze-high-ranking-landing-pages`
- Category: SEO
- Works with: google_search_console, ahrefs, google_analytics
- Page: https://sequel.sh/skills/analyze-high-ranking-landing-pages

See which pages perform best in organic search: the landing pages that earn the most clicks and impressions, the queries behind each one, and how they compare with competitor estimates from Ahrefs.

Answer "which pages are winning in search, and why" by layering three views of the same URLs. Search Console is the source of truth for your own performance; Ahrefs adds competitive context; GA4 tells you whether the traffic is worth anything.

## Steps

1. **Get your top pages by real search traffic.**
   Call `google_search_console.query` with `dimensions: ["page"]`, a 28-day window, and `rowLimit: 100`. Sort the result by clicks descending. This is ground truth. Use it, not Ahrefs estimates, whenever the site is verified in Search Console.

2. **Get the queries behind the top pages.**
   Re-query with `dimensions: ["page", "query"]` over the same window. Group by page and keep the top ~10 queries per page. This tells you what each page actually ranks for, which is usually broader than what it was written for.

3. **Add competitive context (if Ahrefs is connected).**
   Call `ahrefs.top_pages` with the site as `target` to get estimated traffic value and ranking keyword counts per page. Traffic *value* is the number worth surfacing, because it separates a page pulling 10k low-intent visits from one pulling 500 visits that are worth real money.

4. **Check whether the traffic converts.**
   Call `google_analytics.run_report` with `dimensions: ["landingPage"]`, `metrics: ["sessions", "engagementRate", "conversions"]` (add `"totalRevenue"` for ecommerce), same date range. Join to the Search Console pages on the URL path.

5. **Report.**
   One table, one row per page: clicks, impressions, average position, top query, engagement rate, conversions, and Ahrefs traffic value where available. Sort by clicks. Then call out the two or three pages with high clicks but weak engagement or conversions. Those are the real findings, not the leaderboard itself.

## Gotchas

- **Join on the URL path, not the full URL.** Search Console returns absolute URLs (`https://example.com/pricing`); GA4's `landingPage` is a path (`/pricing`). Normalize both to the path, strip trailing slashes and query strings, before joining.
- **Search Console data lags 2 to 3 days.** Never end the window at today or you'll report a fake cliff. End it 3 days back.
- **Search Console truncates.** Rows are capped and low-volume queries are omitted for privacy, so per-page query clicks won't sum to the page total. Don't present the gap as missing traffic.
- **Ahrefs traffic is modelled, not measured.** When both are available, quote Search Console for your own numbers and use Ahrefs only for value estimates and competitor comparison. If they disagree, say so rather than averaging them.
- **Average position is an average.** A page at "position 8.4" may rank 3rd for its main term and 40th for a long tail. Don't optimize off it without looking at the per-query rows.

---

## Skill: Audit internal links to important pages

- Slug: `internal-link-audit`
- Category: SEO
- Works with: google_search_console
- Page: https://sequel.sh/skills/internal-link-audit

Audit the site's own internal linking: crawl navigation and body links between the site's pages to find valuable pages with too few inbound internal links, orphan pages, and the handful of cross-links that would lift striking-distance rankings.

Internal links are the one ranking signal a site controls completely, and the most under-used. Pages that earn impressions but sit at position 12 with two internal links are the cheapest ranking gains on the site. This playbook finds them and names the links to add.

Sequel supplies the traffic data. The crawl uses your own browsing.

## Steps

1. **Build the target list.**
   `google_search_console.query` with `dimensions: ["page"]`, 90 days ending 3 days ago, `rowLimit: 1000`, and with `dimensions: ["query", "page"]`, `rowLimit: 5000`. Add the sitemap URLs. This is every page the site or Google considers real.

2. **Crawl a bounded set.**
   Start from the homepage and the top 100 pages by clicks. Fetch each, extract every same-host `<a href>`, and record source, target, anchor text. Cap the crawl at a few hundred pages and state the cap. Use `sequel_workbench` for the loop if you can execute code.

3. **Separate template links from editorial links.**
   A link that appears on most crawled pages (navigation, footer, sidebar) is a template link. Count it, but count it separately; editorial links from within body content carry more meaning. A simple rule: a target linked from over 80% of crawled pages is template-linked.

4. **Count inlinks per target**, total and editorial, and join to the Search Console data by path.

5. **Find the findings.**
   - **Orphans**: pages in Search Console or the sitemap with zero inlinks in the crawl. If they earn clicks, they are hanging on external links alone.
   - **Under-linked opportunities**: pages with meaningful impressions at position 8 to 20 and fewer than three editorial inlinks. These are the ranking gains.
   - **Generic anchors on important targets**: "click here", "read more", "learn more" on pages that should be described by their topic.
   - **Hubs**: high-traffic pages with few outbound editorial links, which could carry links to the under-linked pages.

6. **Recommend specific links.**
   Ten recommendations, each: from page A (a hub or a topically related page with traffic) to page B (an under-linked opportunity), with the anchor text drawn from B's top query. Say what makes A a sensible source.

## Gotchas

- **A crawl of a few hundred pages is a sample.** Inlink counts are lower bounds. Say so, and raise the cap for a large site if the user wants precision.
- **Links behind JavaScript may be invisible** to a plain fetch. If the navigation seems to have no links, that is why; note it rather than reporting the whole site as orphaned.
- **Do not add links for the sake of it.** A link the reader would not click is a link Google discounts. Every recommendation should make sense on the source page.
- **Redirected or canonicalised targets** should be linked at their final URL. Continue with `redirect-chain-audit` if the crawl found links pointing at redirects.
- **Search Console lags 2 to 3 days.** End the window 3 days back.

---

## Skill: Audit or draft an llms.txt

- Slug: `llms-txt-audit-and-draft`
- Category: SEO
- Works with: google_search_console
- Page: https://sequel.sh/skills/llms-txt-audit-and-draft

Check whether the site has an llms.txt, whether it follows the spec and points at the pages that matter, and if it is missing draft one from the pages Search Console shows people actually find, so AI assistants get a curated map of the site instead of guessing.

An llms.txt is a short markdown file at the site root that tells AI assistants what the site is and which pages to read first. It is the AI-era equivalent of a curated sitemap. Adoption by crawlers is uneven, so it is a cheap bet rather than a guarantee, but a good one takes an hour and a bad one misleads every assistant that does read it.

Sequel supplies the traffic data that decides what belongs in the file. Fetching the existing file and the pages uses your own browsing.

## Steps

1. **Fetch what exists.**
   Request `/llms.txt` and `/llms-full.txt`. A 404 on the first means you are drafting; skip to step 4. If it exists, keep going.

2. **Validate the structure.**
   The spec expects: one H1 with the site or project name, a blockquote with a one-paragraph summary, optional prose, then H2 sections each containing a list of `- [Title](url): description` entries, with an optional final section named `Optional` for lower-priority links. Report deviations.

3. **Validate the links and the coverage.**
   Fetch every listed URL and check it returns 200, is not `noindex`, and is the canonical form. Then pull `google_search_console.query` with `dimensions: ["page"]`, 90 days ending 3 days ago, `rowLimit: 300`, and list the top 50 pages by clicks that the file does not mention. Those are the omissions that matter.

4. **Draft, if missing.**
   From the same Search Console call, take the top pages by clicks and group them into sections that match how the site is organised (documentation, guides, product, pricing, blog). Fetch each chosen page to write a one-line description from its title and meta description in your own words. Keep the main file under about 100 links; push the rest to `Optional` or to `llms-full.txt`. Do not include gated, private, login or utility URLs.

5. **Report.**
   For an audit: the structural findings, the broken or non-canonical links, and the missing top pages. For a draft: the complete file in a markdown code block, ready to save at the site root, plus a note on where it goes and that it does not replace the XML sitemap. Continue with `ai-crawler-access-check` if the crawlers that would read it are blocked.

## Gotchas

- **Do not promise citations.** The file helps assistants that read it; not all do. Present it as low-cost hygiene.
- **Curate, do not enumerate.** A file listing every URL is a sitemap with extra steps. The value is the selection and the descriptions.
- **Keep it maintained.** A stale llms.txt pointing at moved pages is worse than none. Suggest regenerating it when the top pages change.
- **Markdown versions of pages are a separate convention.** Some sites also serve `.md` copies of pages; that is optional and not required for llms.txt to be useful.
- **Search Console lags 2 to 3 days.** End the window 3 days back.

---

## Skill: Audit redirect chains and loops

- Slug: `redirect-chain-audit`
- Category: SEO
- Works with: google_search_console, ahrefs
- Page: https://sequel.sh/skills/redirect-chain-audit

Follow every redirect on the URLs Search Console still sends traffic to, on the sitemap, and on backlinked pages, to find chains, loops, redirects to the homepage, and temporary redirects that should be permanent, so link equity and crawl budget stop leaking.

Every hop in a redirect chain costs a little authority and a little crawl budget, and a chain that ends on the homepage or a 404 costs all of it. Chains accumulate silently through migrations, https moves and trailing-slash changes. This playbook finds them on the URLs that still matter.

Sequel supplies the URL lists. Following the redirects uses your own browsing.

## Steps

1. **Collect the URLs worth testing.**
   Three sources, deduplicated:
   - Old URLs that still get impressions: `google_search_console.query` with `dimensions: ["page"]` over the last 16 months ending 3 days ago, `rowLimit: 5000`. Search Console keeps about 16 months, and a URL that was redirected a year ago often still appears here.
   - The sitemap, fetched via `/robots.txt`.
   - If Ahrefs is connected, the `url_to` column of `ahrefs.backlinks` with the site as `target`, `limit: 1000`. Backlinked URLs are the ones where a chain costs the most.

2. **Follow each URL without auto-following.**
   Request each URL, record the status and `Location` header, then request the location, up to ten hops. Record the full path. Use `sequel_workbench` for the loop if you can execute code. State how many URLs you tested.

3. **Classify what you found.**
   - **Chain**: two or more hops. Note the pattern (http to https to www to trailing slash is the classic four-hop chain).
   - **Loop**: any URL that reappears in its own path.
   - **Temporary where permanent belongs**: 302 or 307 on a URL that has been moved for good.
   - **Redirect to the homepage**: Google treats mass redirects to the homepage as soft 404s. These lose the relevance of the original URL.
   - **Redirect to an error**: chains ending in 4xx or 5xx.
   - **Sitemap URLs that redirect at all**: the sitemap should list final URLs.

4. **Weight the findings.**
   Attach Search Console impressions and, where available, the number of backlinks pointing at each URL. Sort by that. A four-hop chain on a URL with no impressions and no links is trivia; a two-hop chain on the most-linked URL on the site is the top finding.

5. **Report.**
   A table sorted by weight: original URL, hops, final URL, final status, classification, impressions, backlinks, fix. Fixes are specific: "redirect A directly to D", "change 302 to 301", "replace the sitemap entry with D", "update internal links to D". Then the sitemap findings and the loop list separately, since loops are outages.

## Gotchas

- **Use GET, not HEAD.** Some servers answer HEAD differently or not at all.
- **A redirect is not automatically wrong.** The finding is the chain, the loop or the wrong target, not the existence of a redirect.
- **Do not recommend removing a redirect** for a URL with backlinks or impressions. Shorten the chain instead.
- **Trailing-slash and case variants** are the most common hidden chains. Test both forms of the top URLs deliberately.
- **Fetch politely.** A few hundred requests is fine; add a small delay and stop if the server starts returning 429.

---

## Skill: Audit referring domain quality

- Slug: `referring-domain-quality-audit`
- Category: SEO
- Works with: ahrefs
- Page: https://sequel.sh/skills/referring-domain-quality-audit

Grade the domains linking to a site by authority, traffic and link count, spot concentration risk and link-farm patterns, and separate the referring domains that carry weight from the ones that are noise.

A backlink count is nearly meaningless on its own. Two hundred links from one directory and two hundred links from two hundred real sites are different profiles with the same number. This playbook reads the profile at the domain level, where quality and risk actually live.

## Steps

1. **Pull the referring domains.**
   `ahrefs.referring_domains` with the site as `target`, `limit: 1000`, `order_by: "domain_rating:desc"`. You get domain, domain rating, the domain's own organic traffic, how many links it points at the site, and first and last seen dates.

2. **Describe the distribution.**
   Bucket by domain rating (0 to 20, 21 to 40, 41 to 60, 61 and up) and report count and share per bucket. Do the same for the domain's organic traffic (zero, under 1k, 1k to 10k, over 10k). A healthy profile has a long tail of small sites and a meaningful minority above DR 40 with real traffic.

3. **Measure concentration.**
   Share of total links from the single largest referring domain, and from the top five. Treat a domain with hundreds of links to the site as one relationship (a sitewide footer or blogroll), not hundreds of endorsements. A profile where one relationship supplies most of the links is fragile: losing it moves rankings.

4. **Flag the patterns that usually mean nothing or worse.**
   - High domain rating with near-zero organic traffic: the signature of a link network or an expired domain that was bought for its metrics.
   - Very high link counts from one domain: sitewide placement. Counts once.
   - Clusters of domains that first appeared in the same week: a campaign, paid or otherwise. Note the date.
   - A large share of domains with no traffic at all: low-value directories and scraper sites. Harmless, but not an asset.

5. **Compare against one competitor (optional).**
   Repeat step 1 for a competitor the user names and put the two distributions side by side. The useful finding is which DR band the site is missing relative to a competitor that outranks it.

6. **Report.**
   The distribution table, the concentration numbers, the top 20 referring domains by DR with their traffic, the flagged list with the reason for each flag, and a plain statement of what the profile is missing. Recommend outreach toward the band the site lacks. Do not recommend a disavow unless the user has a manual action in Search Console.

## Gotchas

- **Domain rating is Ahrefs' estimate**, not a Google metric. Use it to rank domains against each other, never as a score Google sees.
- **Third-party link indexes lag by weeks.** A domain that stopped linking last week still shows. Read `last_seen` before treating a link as live; continue with `lost-backlink-recovery` for the stale ones.
- **Do not disavow on the strength of this audit.** Google ignores most low-quality links on its own. A disavow file is for manual actions, and a careless one removes links that were helping.
- **Nofollow links still send visitors.** A nofollow link from a high-traffic site is worth more than a dofollow one from a dead site. Traffic is in the table for that reason.
- **The 1000-row cap is the top slice.** On a site with tens of thousands of referring domains, sorted by DR, the tail is not in the result. Say so.

---

## Skill: Audit titles and meta descriptions

- Slug: `metadata-audit`
- Category: SEO
- Works with: google_search_console
- Page: https://sequel.sh/skills/metadata-audit

Compare the title and meta description of each top page against the queries it actually ranks for in Search Console, and find missing, duplicate, truncated or misaligned metadata on the pages where a rewrite would earn the most clicks.

Titles and descriptions are the only part of a page a searcher sees before clicking, and they are usually written before anyone knew what the page would rank for. Search Console tells you what it ranks for now. This playbook lines the two up.

Sequel supplies the query data. Reading the live titles uses your own browsing.

## Steps

1. **Pull the top pages and their queries.**
   `google_search_console.query` with `dimensions: ["page"]`, 28 days ending 3 days ago, `rowLimit: 200`, then with `dimensions: ["query", "page"]`, `rowLimit: 5000`. For each page keep its top five queries by impressions.

2. **Fetch each top page's head.**
   Take the top 50 pages by impressions (state the number). Fetch each and extract the `title`, the meta description, the first H1, and `og:title`. Use `sequel_workbench` to loop if you can execute code.

3. **Run the checks per page.**
   - Missing title or description.
   - Title or description duplicated across two or more pages (templates cause this).
   - Title over 60 characters or description over 155, which is roughly where Google truncates.
   - Title that starts with the brand name, pushing the topic out of view.
   - Title that contains none of the words from the page's top query.
   - Description that does not answer or mention the top query.
   - H1 that targets a different intent than the title.

4. **Prioritise by clicks at stake.**
   Weight each finding by the page's impressions. Then compare the page's CTR to the site's median CTR at its rounded position (the method in `ctr-underperformers`); a page with a metadata finding and below-median CTR goes to the top.

5. **Report.**
   A table sorted by impressions: page, top query, current title, current description, findings. Then rewrites for the top ten: a title under 60 characters that leads with the topic and includes the top query's words naturally, and a description under 155 that answers the query's intent. Show the current and proposed side by side.

## Gotchas

- **Google rewrites titles it does not like**, often when the title is stuffed, too long or duplicated. A clean, specific title is the best defence; do not respond to rewriting by stuffing harder.
- **Templates produce duplicates by design.** A pattern like "Product | Brand" across a catalogue needs the template fixed, not each page.
- **Character counts are a proxy for pixel width.** Wide letters truncate sooner. Treat 60 and 155 as guidance.
- **Titles rendered by JavaScript may not be in the fetched HTML.** If the title element is empty in the source but present in the browser, say so; Google can render it, but it is slower and riskier.
- **Search Console lags 2 to 3 days.** End the window 3 days back.

---

## Skill: Backlink profile health check

- Slug: `backlink-profile-health-check`
- Category: SEO
- Works with: ahrefs
- Page: https://sequel.sh/skills/backlink-profile-health-check

See who links to the site and whether the backlink profile is healthy: referring domains, authority distribution, anchor text mix, dofollow share and concentration risk from Ahrefs, optionally compared against a competitor.

Link count is a vanity metric. What matters is how many distinct domains link to you, how authoritative they are, and whether the anchor text looks natural.

## Steps

1. **Get the baseline.**
   Call `ahrefs.domain_overview` for the target to establish domain rating and organic traffic. Everything below is read relative to this.

2. **Pull referring domains.**
   `ahrefs.referring_domains` with `order_by` on domain rating descending, `limit: 200`. Referring *domains* matter far more than total backlinks: 500 links from 5 sites is a much weaker profile than 100 links from 100 sites.

3. **Pull individual backlinks for anchor analysis.**
   `ahrefs.backlinks` with `limit: 500` ordered by source domain rating. Aggregate anchor text into buckets: branded, exact-match commercial, partial-match, generic ("click here", "read more"), and naked URL.

4. **Look for concentration risk.**
   Compute what share of links come from the single largest referring domain, and from the top 5. A profile where one domain supplies most of the links is fragile: losing it moves rankings.

5. **Compare against a competitor if one is named.**
   Run steps 1–2 for the competitor. Referring-domain count and average domain rating are the two numbers worth comparing directly.

6. **Report.**
   Lead with: total referring domains, median domain rating, share of links from the top domain, and the anchor-text distribution. Then flag anything anomalous.

## Gotchas

- **A high exact-match anchor share is a risk signal.** Natural profiles are dominated by branded and generic anchors. If exact-match commercial anchors are a large share, say so plainly. It's the classic footprint of paid or manipulated links.
- **Don't recommend disavowing on this data alone.** Ahrefs shows links, not penalties. Flag suspicious patterns for human review; disavow files cause real damage when applied carelessly.
- **New links take weeks to appear** in any third-party index. Absence of a recent link isn't evidence it doesn't exist.
- **Dofollow vs nofollow matters** for authority, but nofollow links from high-traffic sites still drive referral visits. Don't dismiss them outright.
- **To grow the profile rather than audit it**, continue with `competitor-new-backlinks`: the sites linking to competitors right now are the prospect list.

---

## Skill: Check AI crawler access

- Slug: `ai-crawler-access-check`
- Category: SEO
- Works with: google_search_console
- Page: https://sequel.sh/skills/ai-crawler-access-check

Check whether ChatGPT, Claude, Perplexity, Gemini and other AI assistants can crawl and read the site or docs, and whether anything needs to be done so they pick it up: parse robots.txt and meta robots for GPTBot, OAI-SearchBot, ClaudeBot, PerplexityBot, Google-Extended and Bingbot, so allowing or blocking them is a deliberate decision rather than an accident.

Most sites have never decided whether AI assistants may read them; the answer is whatever the robots.txt happened to say. Some crawlers train models, some power search answers that cite the site, and blocking the second kind by accident removes the site from a growing share of discovery. This playbook makes the current state explicit.

Sequel supplies the list of pages that matter. Reading robots.txt and the pages uses your own browsing.

## Steps

1. **Get the pages that matter.**
   `google_search_console.query` with `dimensions: ["page"]`, 90 days ending 3 days ago, `rowLimit: 200`. These are the pages whose access is worth checking.

2. **Fetch and parse robots.txt.**
   Read `/robots.txt`. For each user agent below, work out whether the root and the top page paths are allowed or disallowed, remembering that a group for `*` applies to any agent without its own group, and the most specific matching rule wins.
   - Answer and search crawlers, whose results cite the site: `OAI-SearchBot`, `ChatGPT-User`, `Claude-SearchBot`, `Claude-User`, `PerplexityBot`, `Perplexity-User`, `Bingbot`, `Googlebot`, `DuckAssistBot`.
   - Training crawlers, which do not send traffic: `GPTBot`, `ClaudeBot`, `anthropic-ai`, `Google-Extended`, `CCBot`, `Bytespider`, `Amazonbot`, `Applebot-Extended`, `meta-externalagent`.

3. **Check the pages themselves.**
   Fetch five to ten of the top pages and record meta robots and `X-Robots-Tag` values, looking for `noindex`, `nosnippet`, `max-snippet`, `noai` and `noimageai`. Note whether the main content is present in the raw HTML; most AI crawlers do not execute JavaScript, so a page that renders client-side is effectively empty to them.

4. **Explain the consequences.**
   Say specifically what each block does. Blocking `GPTBot` keeps the site out of model training and has no effect on ChatGPT search; blocking `OAI-SearchBot` removes it from ChatGPT's search answers. `Google-Extended` controls Gemini training only; AI Overviews use `Googlebot`, and the only controls are `nosnippet` and `max-snippet`, which also affect the normal result. Apply the same reasoning to each agent.

5. **Report.**
   A matrix of agent against access (allowed, disallowed, partially disallowed with the paths), the meta robots findings per page, the JavaScript-rendering observation, and a recommendation in two parts: answer engines that cite the site are usually worth allowing; training crawlers are a policy decision for the user, listed with what each one feeds. Flag any block that looks accidental, such as a `Disallow: /` under `*` with no exceptions.

## Gotchas

- **robots.txt is a request, not a wall.** Well-behaved crawlers honour it; others do not. It is still the right place to state the policy.
- **The edge can block what robots.txt allows.** A CDN or WAF bot rule can return 403 to these agents with nothing in robots.txt. If you can fetch with a custom user agent, test the top pages as `GPTBot` and `PerplexityBot`; if you cannot, say the edge was not tested.
- **Agent names change.** The list above is current at the time of writing. Check each vendor's documentation with your browsing if a name looks stale.
- **Blocking is not neutral.** A site invisible to answer engines loses citations to competitors that allowed them. Present that trade-off, not a recommendation to block by default.
- **Continue with** `ai-referral-traffic` to see what the current policy is producing, and `llms-txt-audit-and-draft` to give allowed crawlers a curated map.

---

## Skill: Check sitemap health

- Slug: `sitemap-health`
- Category: SEO
- Works with: google_search_console
- Page: https://sequel.sh/skills/sitemap-health

Check that the site's XML sitemaps are submitted, fresh, error-free and complete: compare submitted vs indexed counts in Search Console, find sitemap URLs that redirect or return errors, and find pages with search traffic that the sitemap does not list.

A sitemap is a promise to Google about which URLs matter. Broken promises (URLs that redirect or 404, lastmod dates that never change, whole sections missing) make Google trust the file less, and the file is often the only way large or new sections get discovered at all.

Sequel supplies Search Console's view of each sitemap. Reading the XML itself and checking its URLs uses your own browsing.

## Steps

1. **List what Search Console knows.**
   `google_search_console.list_sitemaps`. For each: type, whether it is an index, pending state, last submitted, last downloaded, warning and error counts, submitted and indexed totals. If any row is a sitemap index, call again with `sitemapIndex` set to its path to see the children. Use `google_search_console.get_sitemap` on any sitemap with errors for the detail.

2. **Read the XML.**
   Fetch each sitemap (or the index and its children) with your browsing. Count the URLs, note the `lastmod` values, and compare the URL count to the submitted total Search Console reports.

3. **Check a sample of listed URLs.**
   Thirty to fifty URLs spread across the file. For each: does it return 200, is its canonical itself, is it free of noindex. Any redirect, error or noindex in a sitemap is a finding. Use `sequel_workbench` for the loop if you can execute code.

4. **Check completeness against traffic.**
   `google_search_console.query` with `dimensions: ["page"]`, 90 days ending 3 days ago, `rowLimit: 2000`. Pages with clicks that are absent from every sitemap are the pages the sitemap forgot.

5. **Check freshness.**
   A last downloaded date weeks old on a sitemap that changes often, a `lastmod` that is the same on every URL, or a `lastmod` in the future all tell Google the dates are meaningless. Note which applies.

6. **Report.**
   Per sitemap: submitted, indexed, gap, errors, warnings, last downloaded, URL count from the XML. Then the findings: listed URLs that redirect or error (with the final URL), listed URLs carrying noindex, pages with traffic missing from the sitemap, and freshness problems. Fixes are specific: remove or replace listed URLs, add the missing pages, generate real `lastmod` values. Continue with `index-coverage-review` if the indexed gap is large.

## Gotchas

- **Indexed counts in the sitemap report lag and are per sitemap.** A URL listed in two sitemaps is counted twice.
- **Sitemaps should list only canonical, indexable, 200 URLs.** That is the whole rule; every finding above is a violation of it.
- **`lastmod` only helps when it is honest.** Google has said it ignores the field on sites that set it to the current time on every URL.
- **Limits are 50,000 URLs and 50 MB uncompressed per file.** A file near either should be split.
- **Search Console lags 2 to 3 days.** End the window 3 days back.

---

## Skill: Check whether organic traffic converts

- Slug: `organic-landing-page-conversion`
- Category: SEO
- Works with: google_search_console, google_analytics
- Page: https://sequel.sh/skills/organic-landing-page-conversion

Find which pages get search traffic but few signups, orders or purchases: join Search Console pages to GA4 landing-page engagement and conversions to see which organic pages, product pages and category pages actually convert, which attract visits that bounce, and where the gap between ranking and outcome is widest.

Clicks are the SEO team's number; conversions are the company's. A page can be an SEO success and a business failure at the same time, and only the join between Search Console and GA4 shows which is which.

## Steps

1. **Pull organic performance by page.**
   `google_search_console.query` with `dimensions: ["page"]`, a 28-day window ending 3 days ago, `rowLimit: 500`. Clicks, impressions and position per URL.

2. **Pull outcomes by landing page.**
   `google_analytics.run_report` with `dimensions: ["landingPage"]`, `metrics: ["sessions", "engagementRate", "conversions", "totalRevenue"]`, the same dates, and `dimensionFilter` on `sessionMedium` `EXACT` `organic`. Add `ecommercePurchases` for a store.

3. **Join on the URL path.**
   Search Console returns absolute URLs; GA4 returns paths. Strip the origin, the query string and the trailing slash from both, lowercase, then join. Report how many Search Console pages found no GA4 match; a large share usually means a redirect or a canonical mismatch worth its own look.

4. **Compute the outcome metrics.**
   Per page: clicks, sessions, engagement rate, conversions, conversion rate (conversions over sessions), and revenue if present. Also the ratio of GA4 sessions to Search Console clicks; expect 0.6 to 0.9. A page far outside that range has a tracking problem, not a content problem.

5. **Sort pages into four groups.**
   - High clicks, low conversion rate: the page attracts the wrong intent, or the page fails the right one. Fetch it with your browsing and say which.
   - High conversion rate, low clicks: the page works; it needs more visibility. Continue with `find-striking-distance-keywords` and `internal-link-audit` for it.
   - High on both: protect. Do not recommend changes to it.
   - Low on both: ignore for now.

6. **Report.**
   One table, one row per page, sorted by clicks: clicks, position, sessions, engagement rate, conversions, conversion rate, revenue. Then the two interesting groups (high traffic low conversion, high conversion low traffic) with two or three named pages each and a specific action per page. State which conversion events GA4 is counting.

## Gotchas

- **"Conversions" means whatever key events the account has configured.** Check with the user what counts before reading the numbers; a site with "scroll 90%" as a key event will look like it converts everything.
- **Sessions and clicks are different measurements.** Consent banners, ad blockers, bots and GA4 sampling all widen the gap. Do not present the gap as lost visitors.
- **Organic medium is not only Google.** GA4's `organic` includes Bing, DuckDuckGo and the rest. Search Console is Google only. The join is approximate for that reason; say so.
- **GA4 lags about a day**, Search Console two to three. Ending both windows 3 days back keeps them comparable.
- **Engagement rate is a weak proxy for intent.** A page that answers the question fully and sends the reader away has low engagement and did its job. Use conversions where they exist.

---

## Skill: Competitor keyword gap analysis

- Slug: `competitor-keyword-gap`
- Category: SEO
- Works with: ahrefs, google_search_console
- Page: https://sequel.sh/skills/competitor-keyword-gap

Compare what competitors rank for against your own coverage to find high-value keywords you're missing entirely.

A gap analysis answers "what are they getting traffic for that we aren't?" The output should be a prioritized list of topics, not a raw keyword dump.

## Steps

1. **Confirm the competitor list.** If the user hasn't named competitors, ask; don't guess. Search competitors are often not business competitors.

2. **Pull each competitor's organic keywords.**
   Call `ahrefs.organic_keywords` for each competitor domain with `mode: "domain"`, ordered by traffic, `limit: 500`. Keep the same `country` across all calls or the comparison is meaningless.

3. **Pull your own coverage.**
   Same call for the user's domain. If Search Console is connected, also pull `google_search_console.query` with `dimensions: ["query"]` over 28 days. It will show terms you rank for that Ahrefs missed, which prevents false "gaps".

4. **Compute the gap.**
   Keywords where at least one competitor ranks in the top 10 and you rank below 20 or not at all. Weight by volume and by how many competitors rank. A term two or three competitors all cover is a validated topic, not a one-off.

5. **Cluster before reporting.**
   Group the gap keywords into topics rather than listing them individually. Fifty keywords usually collapse into five or six content themes, and that's the actionable form.

6. **Report.**
   Per cluster: the theme, total addressable volume, which competitors rank, their best position, average difficulty, and the single best head term. Then name the top 3 clusters worth pursuing and why.

## Gotchas

- **Ahrefs volume and traffic are estimates.** Present them as directional. Never mix an Ahrefs estimate and a Search Console actual in the same column without labelling which is which.
- **Set `country` deliberately.** It defaults to a single market; a US-only pull will look like a huge gap for a UK-focused competitor.
- **Not every gap is worth closing.** Filter out terms irrelevant to the business before reporting. A competitor's unrelated blog traffic is not an opportunity.
- **Check difficulty against your actual authority.** Use `ahrefs.domain_overview` on both domains first. If a competitor has a far higher domain rating, their easy terms are not your easy terms.

---

## Skill: Content decay audit

- Slug: `content-decay-audit`
- Category: SEO
- Works with: google_search_console, google_analytics
- Page: https://sequel.sh/skills/content-decay-audit

Identify pages whose search traffic is declining over time so refreshes go to the content that's actually slipping.

Content decay is gradual: a page that ranked well slowly loses position as competitors publish fresher material. It rarely shows up in a single-period report. You have to compare two windows.

## Steps

1. **Pick two comparable windows.**
   Use two 90-day windows: the recent one ending 3 days ago, and the same 90 days from the prior year where seasonality is a factor (otherwise the immediately preceding 90 days). State which comparison you used and why.

2. **Pull page-level performance for both windows.**
   Two `google_search_console.query` calls with `dimensions: ["page"]`, `rowLimit: 500`, one per window.

3. **Compute the deltas.**
   Join on page URL. For each page calculate the change in clicks, impressions, and average position, in both absolute and percentage terms. Sort by absolute click loss; percentage alone will surface trivial pages that dropped from 4 clicks to 1.

4. **Separate the causes.** The delta pattern tells you what happened:
   - Impressions flat, position worse, clicks down → **ranking decay**. Competitors overtook you. Refresh the content.
   - Impressions down, position flat → **demand decline**. Fewer people are searching. Not a content problem; don't recommend a rewrite.
   - Impressions up, position flat, CTR down → **SERP change**. An AI overview or new feature is absorbing clicks. Snippet edits help; content depth may not.

5. **Confirm with engagement.**
   `google_analytics.run_report` with `dimensions: ["landingPage"]`, `metrics: ["sessions", "engagementRate", "conversions"]` for the recent window. A decaying page that still converts well is a higher-priority fix than one that never converted.

6. **Report.**
   Table sorted by clicks lost: page, clicks before/after, delta, position change, diagnosed cause, and current conversions. Recommend refreshes only for the ranking-decay group, and say explicitly which pages you are *not* recommending work on, and why.

## Gotchas

- **Don't diagnose decay from one window.** A page down this month may just be seasonal.
- **Check for site changes first.** A URL that changed, got redirected, or was noindexed will look like catastrophic decay. If a page's clicks went to near zero abruptly rather than gradually, suspect a technical cause and say so.
- **Year-over-year is the honest comparison for seasonal businesses.** Comparing Q4 to Q3 will always look like decay for retail.
- **Search Console keeps ~16 months of data.** A year-over-year comparison near the edge of that window may return partial data.

---

## Skill: Debug why a page is not indexed

- Slug: `page-not-indexed-debug`
- Category: SEO
- Works with: google_search_console
- Page: https://sequel.sh/skills/page-not-indexed-debug

Find out why a specific page is missing from Google, or used to show up in search and has now disappeared: inspect its index status and coverage state, the canonical Google chose, robots.txt and noindex blocks, fetch errors and sitemap membership, then check the live page, and end with the one fix that applies.

"Why isn't this page indexed" has about eight possible answers, and Search Console's URL Inspection API gives most of them directly. The job is to read the inspection result correctly, confirm it against the live page, and stop at one diagnosis.

## Steps

1. **Confirm the URL belongs to the connected property.**
   The URL must be under the property this connection is pinned to. If the site has both a domain property and a URL-prefix property, or www and non-www, run `google_search_console.list_sites` and check. An inspection against the wrong property fails or lies.

2. **Inspect the URL.**
   `google_search_console.inspect_url` with the URL (up to ten per call if several pages are affected). Read every column: verdict, coverage state, indexing state, robots.txt state, page fetch state, last crawl time, the Google canonical against the user canonical, sitemaps, referring URLs, and rich result issues.

3. **Diagnose from the coverage state.**
   - **"URL is unknown to Google"**: Google has never seen it. Not in any sitemap and no referring URLs is the usual reason. Continue with `sitemap-health` and `internal-link-audit`.
   - **"Discovered - currently not indexed"**: Google knows the URL and has not bothered to crawl it. A priority problem: too few internal links, or a very large site. Internal links and a fresh sitemap entry are the levers.
   - **"Crawled - currently not indexed"**: Google fetched it and chose not to index it. The hardest case. Usually thin or near-duplicate content, or a page type Google has decided is low value. Check whether similar pages on the site are indexed and how this one differs.
   - **Blocked by robots.txt** (robots.txt state DISALLOWED or fetch state BLOCKED_ROBOTS_TXT): fetch `/robots.txt` yourself and find the matching rule.
   - **Excluded by noindex** (indexing state BLOCKED_BY_META_TAG or BLOCKED_BY_HTTP_HEADER): fetch the page and confirm the tag or header. Ask whether it is intended before calling it a bug.
   - **Fetch errors** (NOT_FOUND, SERVER_ERROR, SOFT_404, REDIRECT_ERROR, ACCESS_DENIED): fetch the page now and compare with the last crawl time. If it is fine now, the error was transient and a recrawl fixes it; if not, the page is broken.
   - **Google chose a different canonical** (google canonical differs from user canonical, or the coverage state says so): Google decided another URL is the real one. Inspect that URL too. Either the site's canonical tag is wrong, or the two pages are near-duplicates and Google is right.
   - **"Alternate page with proper canonical tag"**: working as intended. The canonical is indexed instead. Not a problem unless the canonical is wrong.

4. **Check the live page with your own browsing.**
   Status code, the canonical tag, meta robots and `X-Robots-Tag`, and whether the main content is present in the raw HTML. Compare with what Google saw at the last crawl time; a fix deployed after that time has not been seen yet.

5. **Check whether it ever earned impressions.**
   `google_search_console.query` with `dimensions: ["page", "date"]` over 90 days ending 3 days ago and a `filters` entry with `dimension: "page"`, `operator: "equals"` and the URL. A page that had impressions and then stopped was indexed and dropped; a page with none may never have been.

6. **Report.**
   Per URL: the coverage state, the diagnosis in one sentence, the evidence columns that support it, the single fix, and how to verify it: re-inspect after the next crawl, which cannot be scheduled and may take days to weeks. Do not promise when.

## Gotchas

- **Inspection reports the indexed version, not the live one.** If the page changed after the last crawl time, the result describes the old page.
- **The API is quota-limited per property.** A handful of URLs is the right scale; for a whole section of the site continue with `index-coverage-review`, which samples.
- **Not indexed can be correct.** Tag pages, filtered listings, thin variants and paginated archives are often better left out. Say when the state looks intended.
- **A canonical pointing at another page the site owns is not a bug by itself.** It is a decision; the question is whether it is the right one.
- **Property mismatches produce confident nonsense.** Inspecting `https://www.example.com/page` against a property for `https://example.com/` returns an error or an unrelated result.

---

## Skill: Find a competitor's new backlinks and turn them into prospects

- Slug: `competitor-new-backlinks`
- Category: SEO
- Works with: ahrefs, google_search_console
- Page: https://sequel.sh/skills/competitor-new-backlinks

Find the new backlinks a competitor has earned recently, where they came from and how they got them, then turn the pattern into a prospect list: which sites are linking to competitors in your space right now, which competitor pages attract links, and which of those sites already link to a rival but not to you.

A competitor's newest backlinks are the most useful ones to study. Old links tell you what worked once; links from the last few weeks tell you which sites are linking to businesses like yours right now, which of the competitor's pages are earning them, and which tactics they are running. Every one of those sites is a prospect with proof attached.

Ahrefs supplies the link data. Reading the linking pages to classify how each link was earned uses your own browsing.

## Steps

1. **Confirm the competitor and the window.**
   If the user has not named a competitor, ask; search competitors are often not the business competitors they have in mind. Default the window to the last 90 days, or 30 days for a space where links move fast. Say which you used.

2. **See which of their pages are earning links.**
   `ahrefs.top_pages_by_links` with the competitor as `target` and `since` set to the window start. It ranks their pages by new links in the window. Fetch the top ten with your browsing and classify each by format (guide, tool or calculator, research or data, comparison, template, glossary, product). This is what content in this market is worth linking to.

3. **Pull the new links themselves.**
   `ahrefs.backlinks` with the competitor as `target`, `since` set to the window start, `dofollow_only: true`, `content_only: true`, `one_per_domain: true`, `min_domain_rating: 20` (raise it for a strong site), `limit: 300`, `order_by: "domain_rating_source:desc"`. Leave `include_traffic` off for now; it costs ten times as much per row and is only worth spending on the shortlist.

4. **Classify how each link was earned.**
   For the top 40 by domain rating, read the referring page's `title`, `anchor` and `links_external`, and fetch the page. Assign one type: listicle or roundup ("best X tools"), guest post, resource page, review or comparison, editorial mention, directory or citation, syndication or scraper, forum or community, sponsorship or partner page, press pickup. Report the mix as a percentage. The mix is the finding: it says which tactic the competitor is running and which asks are realistic for you.

5. **Find the gap: sites that link to them and not to you.**
   `ahrefs.referring_domains` for the competitor and again for the user's own site, both with `since` set to the window start (or omitted for the whole profile), `dofollow_only: true`, `exclude_spam: true`, `min_domain_rating: 20`, `limit: 1000`. Diff on `domain`. Domains in the competitor's set and absent from ours are prospects. With two or three competitors, a domain that links to more than one is the highest priority: it demonstrably links to this kind of site.

6. **Shortlist and price the shortlist.**
   Keep 25 to 40 prospects. Rerun `ahrefs.referring_domains` for those domains only with `include_traffic: true` if the user wants traffic in the table, or skip it and say so. Rank by domain rating, then link type ease (listicle and resource page first, guest post next, editorial mention last), then recency (a page that added a competitor link this month is being actively edited), then a one-line relevance check from your fetch.

7. **Report.**
   Three parts. First, the competitor's link-earning pages with their format, so the user knows what to build. Second, the link-type mix with two or three example links per type. Third, the prospect table: linking domain, DR, the competitor page it links to, the linking page, why the link exists (type), the suggested ask, and the page on our site the link should point to. Set aside sitewide, nofollow and syndication rows in a separate short list so the reader knows they were seen and skipped. Continue with `backlink-profile-health-check` for the user's own profile and `competitor-top-pages-teardown` for the content side.

## Gotchas

- **"New" means newly found by Ahrefs.** `first_seen_link` is the discovery date, not the publish date. A link can be weeks old before the index sees it, and the index lags more on small sites. Treat the window as approximate.
- **One link per domain is on for a reason.** A sitewide footer or blogroll link shows up on every page of the referring site. Without `one_per_domain` a single relationship looks like hundreds of links.
- **Syndication inflates the count.** The same anchor and title across many low-DR domains is one article republished by scrapers, not many endorsements. Group by anchor and title before reading the mix.
- **A competitor's spam is not a strategy.** `exclude_spam` and the DR floor exist so the prospect list is not a copy of their link farm. Do not recommend buying links or directory blasts, whatever the competitor did.
- **Units add up.** Every Ahrefs row costs units, and `traffic_domain` costs ten per row. Pull the wide list without traffic, then price only the shortlist.
- **Ahrefs numbers are estimates.** DR and traffic rank prospects against each other; they are not a forecast of what a link will do. Never promise rankings from links.
- **Relevance beats DR.** A DR 35 site that writes about the user's category is a better prospect than a DR 70 general-news site that linked to a competitor's funding announcement once. Say which is which.

---

## Skill: Find new and lost search queries

- Slug: `new-and-lost-queries`
- Category: SEO
- Works with: google_search_console
- Page: https://sequel.sh/skills/new-and-lost-queries

Find the new keywords the site started ranking for this month and the search queries it stopped appearing for, by comparing two periods, so you can see which new content is winning and where visibility quietly disappeared.

Period-over-period totals hide churn. A site can hold steady on clicks while gaining a hundred queries and losing a hundred others, and the two lists are more useful than the total: the gains show what is working, and the losses are usually the earliest sign of a problem.

## Steps

1. **Pull both windows.**
   Two calls to `google_search_console.query` with `dimensions: ["query", "page"]` and `rowLimit: 5000`: the last 28 days ending 3 days ago, and the 28 days before that. For seasonal businesses use the same 28 days a year earlier and say so.

2. **Set a floor.**
   Keep rows with at least 20 impressions in whichever window they appear (state the floor). Without one the lists are dominated by single-impression long tail that churns constantly and means nothing.

3. **Diff the query sets.**
   New: queries in the recent window only. Lost: queries in the prior window only. Swapped: the same query in both windows but served by a different page. Sort each list by impressions.

4. **Explain the new list.**
   Group new queries by the page that serves them. A cluster of new queries on one URL usually means a new or updated page has started ranking; name the page and the theme.

5. **Explain the lost list.**
   Group lost queries by their former page. If one page lost many queries, check whether the page still exists and still returns 200 with your own browsing; if it does, continue with `content-decay-audit` for that page. If many pages each lost a few queries, that is diffuse and belongs to `traffic-drop-investigation`.

6. **Report.**
   Three short tables: top new queries (query, page, impressions, position), top lost queries (query, former page, impressions it had, position it had), swapped queries (query, old page, new page). Then two or three sentences on the pattern in each.

## Gotchas

- **Lost usually means "fell below the floor", not "gone".** A query at 25 impressions last month and 15 this month shows as lost. Say the lists are relative to the floor.
- **Row caps bite on large sites.** At 5000 rows a big site is truncated and the diff will invent losses at the tail. If either window returns exactly the cap, raise it or narrow the comparison to a section of the site with a `page` filter.
- **Swapped is a cannibalization signal.** If the same query keeps swapping pages, continue with `keyword-cannibalization-check`.
- **Search Console lags 2 to 3 days.** End the recent window 3 days back.

---

## Skill: Find page-one rankings with weak CTR

- Slug: `ctr-underperformers`
- Category: SEO
- Works with: google_search_console
- Page: https://sequel.sh/skills/ctr-underperformers

Find the queries and pages that already rank on Google's first page but get fewer clicks than their position should earn, so the fastest wins come from rewriting titles and descriptions instead of building new content.

A page-one ranking that nobody clicks is the cheapest problem in SEO to fix. The ranking work is done; the snippet is losing the click. This playbook finds those rows and sizes the clicks being left on the table.

## Steps

1. **Pull query and page rows.**
   `google_search_console.query` with `dimensions: ["query", "page"]`, a 28-day window ending 3 days ago, `rowLimit: 2000`.

2. **Keep page one, with real impressions.**
   Filter to average position 1 to 10 and set an impressions floor (200 over 28 days is sensible; state the one you used). Strip branded queries first: their CTR is naturally very high and will distort everything below.

3. **Build the site's own CTR curve.**
   Round position to the nearest integer and take the median CTR of the remaining rows at each position 1 to 10. Use the site's own curve rather than an industry benchmark: a brand-heavy site or one whose results carry rich snippets has a curve of its own, and a generic one will flag the wrong rows.

4. **Flag the underperformers and size them.**
   A row is an underperformer when its CTR is below half the site median at its position. For each, compute missed clicks as (median CTR minus actual CTR) times impressions. Sort by missed clicks descending. This is the priority list.

5. **Rule out SERP features before blaming the snippet.**
   If rows at positions 1 to 3 are underperforming across the board, the query is probably being answered on the page by a featured snippet or AI overview, and no title rewrite will recover it. Run `google_search_console.query` with `dimensions: ["searchAppearance"]` over the same window to see how much of the site's visibility carries rich result types, and check the live result for the top few flagged queries with your own browsing.

6. **Compare the snippet to the query.**
   Fetch each flagged page and read its title and meta description. The usual failures: the title leads with the brand instead of the topic, the description is a generic boilerplate, the title promises something different from what the query asks, or the page has no description and Google is picking a bad excerpt.

7. **Report.**
   A table sorted by missed clicks: query, page, position, impressions, clicks, CTR, site median CTR at that position, missed clicks. Then concrete rewrites for the top five, with the target query in the title and a description that answers the intent. Keep the SERP-feature cases in a separate group labelled "cannot be fixed by copy".

## Gotchas

- **Never compare CTR across positions.** Position 1 and position 8 have different expected CTRs by a factor of ten. The comparison is always within a position band.
- **An average position hides a split.** A row "at 3.2" may be position 1 on desktop and 8 on mobile. Add `dimensions: ["query", "page", "device"]` before diagnosing the biggest rows.
- **Tiny impression counts produce noisy CTRs.** Ten impressions and zero clicks is not evidence of anything. Keep the floor.
- **Sitelinks and brand queries look like huge CTR.** They are, and they are not opportunities. That is why brand queries are stripped in step 2.
- **Search Console lags 2 to 3 days.** End the window 3 days back.

---

## Skill: Find queries that fell off page one

- Slug: `page-one-losses`
- Category: SEO
- Works with: google_search_console
- Page: https://sequel.sh/skills/page-one-losses

List the search queries that recently dropped out of Google's top ten results between two periods, with the pages that lost them, so ranking losses are caught while they are still cheap to recover.

Position 10 to position 11 is the most expensive single-place move in search: page two receives almost nothing. A query that just slipped over that line is usually recoverable with a modest update, while one that collapsed to position 40 usually is not. This playbook finds both and tells them apart.

## Steps

1. **Pull both windows.**
   Two calls to `google_search_console.query` with `dimensions: ["query", "page"]` and `rowLimit: 5000`: the last 28 days ending 3 days ago, and the 28 days before that.

2. **Join and filter.**
   Join on query and page. Keep rows where the prior position was 10 or better and the recent position is worse than 10, and where the prior window had at least 50 impressions (state the floor). Sort by prior clicks descending so the list leads with what was actually earning traffic.

3. **Separate the slips from the collapses.**
   Recent position 10.5 to 14: a slip, the cheap-to-recover group. Recent position above 20: a collapse, usually a different problem (the page changed, was overtaken, or the query's results page was restructured). Report them as two groups.

4. **Check whether the page is still the page.**
   For each row, look in the recent window for the same query served by a different URL on the site. If one is, the site's own second page has taken over and this is cannibalization; continue with `keyword-cannibalization-check`. If the page received no impressions at all in the recent window, check with your browsing that it still returns 200 and is not noindexed; continue with `technical-crawl-audit` if it is not.

5. **Report.**
   Two tables (slips, collapses): query, page, prior position, recent position, prior clicks, prior impressions, and a one-word diagnosis (slipped, overtaken, cannibalised, technical). Recommend work only for the slips and the cannibalised rows; say what to verify for the collapses.

## Gotchas

- **Position is an average.** A row moving from 9.6 to 10.4 may be the same rankings with a slightly different mix of impressions. Treat moves under one position as noise unless clicks fell too.
- **The floor decides the list.** Fifty impressions is a starting point; on a large site raise it or the list will be hundreds of rows of long tail.
- **A month is short.** Rankings fluctuate. If the user wants confidence rather than an early warning, compare 90-day windows instead.
- **Brand queries do not belong here.** Strip them; a brand query falling off page one is a different kind of emergency.
- **Search Console lags 2 to 3 days.** End the recent window 3 days back.

---

## Skill: Find striking-distance keywords

- Slug: `find-striking-distance-keywords`
- Category: SEO
- Works with: google_search_console, ahrefs
- Page: https://sequel.sh/skills/find-striking-distance-keywords

Find the keywords the site almost ranks on page one for: queries sitting at positions 8 to 20 with real impressions, where a small push produces the biggest traffic gain for the least work.

"Striking distance" keywords already rank, already get impressions, and sit just below the click cliff. Moving one from position 12 to position 6 typically multiplies its clicks several times over, far cheaper than ranking something new.

## Steps

1. **Pull query-level performance.**
   Call `google_search_console.query` with `dimensions: ["query", "page"]`, a 28-day window ending 3 days ago, `rowLimit: 1000`.

2. **Filter to the striking-distance band.**
   Keep rows where average position is between 8 and 20 AND impressions are meaningful (a sensible floor is 100 impressions over 28 days; state the threshold you used). Rank the survivors by impressions descending. Low-impression rows in this band aren't opportunities, they're noise.

3. **Flag low CTR for the position.**
   Within the band, a query whose CTR is well below its peers at a similar position usually has a title/meta problem rather than a ranking problem. Note these separately. They're the fastest wins because they need a copy edit, not a content project.

4. **Add difficulty (if Ahrefs is connected).**
   Pass the shortlisted terms to `ahrefs.keywords_overview` in batches to get keyword difficulty, volume, and CPC. Drop anything with difficulty far above what the site already ranks for. High CPC is a useful proxy for commercial intent.

5. **Report.**
   A table sorted by opportunity: keyword, current position, impressions, clicks, CTR, the page currently ranking, difficulty, and CPC. Group into "fix the snippet" (low CTR for position) and "strengthen the page" (position 11–20). Say what to do for the top 5, not all of them.

## Gotchas

- **One page per keyword.** If two URLs both rank for a term, that's cannibalization, not opportunity. Flag it and hand it to `keyword-cannibalization-check` rather than recommending more content.
- **Position 11 vs 8 matters enormously.** Position 11 is top-of-page-two and gets almost nothing; position 8 is already on page one. Don't treat the band as uniform.
- **Search Console averages positions across devices and countries.** If the site is international, add `dimensions: ["query", "page", "country"]` and check whether one market is dragging the average.
- **Branded queries pollute the list.** Strip queries containing the brand name before ranking. They already convert and can't be improved much.

---

## Skill: Find where paid search overlaps organic

- Slug: `paid-and-organic-overlap`
- Category: SEO
- Works with: google_search_console, google_analytics
- Page: https://sequel.sh/skills/paid-and-organic-overlap

Compare the pages the site already ranks for organically against the landing pages receiving paid search clicks in GA4, to find where ads are buying visits organic would deliver anyway, and where paid traffic reveals demand organic has not captured.

Paid and organic search are usually run by different people looking at different dashboards. Put the two side by side and the same pages show up in both, which raises two useful questions: is the ad spend on those pages incremental, and which paid-only pages point at demand the organic team has not gone after?

## Steps

1. **Pull the organic side.**
   `google_search_console.query` with `dimensions: ["page"]` over 28 days ending 3 days ago, `rowLimit: 500`, and again with `dimensions: ["query", "page"]`, `rowLimit: 5000`.

2. **Pull paid landing pages from GA4.**
   `google_analytics.run_report` with `dimensions: ["landingPage", "sessionMedium"]`, `metrics: ["sessions", "conversions", "totalRevenue"]`, the same dates, `dimensionFilter` on `sessionMedium` `EXACT` `cpc`. Run it a second time with `organic` so paid and organic conversion rates can be compared on the same pages.

3. **Join on the path** after stripping origin, query string and trailing slash from every URL.

4. **Sort the pages into three groups.**
   - **Overlap**: paid sessions on a page whose main organic queries sit at position 1 to 3 with solid clicks. These are the candidates for an incrementality test.
   - **Paid only**: paid sessions, no organic clicks. Paid has found demand organic has not. Pass the page's paid intent to `query-clusters-to-content-plan`.
   - **Organic only**: no action here.

5. **Look at brand specifically.**
   Filter the query rows to brand terms. If the site holds position 1 for its own brand and a brand ad is running, quantify the paid brand sessions separately. This is the most common overlap and the most contested.

6. **Compare conversion on the overlap pages.**
   Paid conversion rate versus organic conversion rate on the same pages. If they are similar, the page converts regardless of channel and the test is about volume; if paid is much higher, the ad copy or the query mix is different and pausing would lose something.

7. **Report.**
   The overlap table (page, organic position on main queries, organic clicks, paid sessions, paid conversions, organic conversions), the paid-only list as opportunities, the brand overlap figure, and a recommendation to test pausing on the two or three strongest overlap pages rather than a blanket cut. Say explicitly that cost is unknown.

## Gotchas

- **There is no cost data here.** GA4 shows paid sessions and conversions, not spend. Without a Google Ads connection, the overlap can be described but not priced; say so plainly.
- **Position 1 organic does not make the ad wasted.** Ads plus organic take more of the results page than either alone, and brand ads block competitors bidding on the brand. The honest recommendation is a test, not a conclusion.
- **`cpc` is every paid search engine and sometimes more.** Anything tagged with medium cpc lands here, including Bing Ads and mis-tagged social. Check `sessionSource` before treating it as Google Ads.
- **Search Console is Google only.** The overlap is Google organic against all paid search, which slightly overstates paid-only pages.
- **Search Console lags 2 to 3 days.** End every window 3 days back.

---

## Skill: Investigate an organic traffic drop

- Slug: `traffic-drop-investigation`
- Category: SEO
- Works with: google_search_console, google_analytics
- Page: https://sequel.sh/skills/traffic-drop-investigation

Find out why organic clicks or traffic fell, exactly when it started, and whether the cause is a ranking loss, a demand change, a technical problem, a Google update or a tracking gap, using Search Console and GA4 evidence instead of guesswork.

"Traffic is down" is a symptom with at least five different causes, and each one needs a different fix. The job here is to establish the date, the segment and the mechanism before anyone rewrites a page.

## Steps

1. **Establish the timeline.**
   `google_search_console.query` with `dimensions: ["date"]` over the last 16 weeks ending 3 days ago. Compute a 7-day rolling average of clicks and impressions and find the inflection: the first day the average leaves its prior baseline. Say whether the shape is a step (sudden, one day) or a slope (gradual, over weeks). A step points at a technical change or an update; a slope points at decay or seasonality.

2. **Confirm the drop is real, not a tracking change.**
   `google_analytics.run_report` with `dimensions: ["date", "sessionMedium"]`, `metrics: ["sessions"]` over the same window, and keep `organic`. If Search Console clicks are flat but GA4 organic sessions fell, the cause is tagging, consent or a filter change, not SEO. Stop and say so.

3. **Isolate the segment.**
   Take the 28 days before the inflection and the 28 days after. Run `google_search_console.query` for each window with `dimensions: ["page"]`, then again with `["query"]`, `["device"]` and `["country"]`. Compute the delta per row. The question is whether the loss is concentrated in a few pages or queries, or diffuse across everything in proportion.

4. **Diagnose from the pattern.**
   - Concentrated, position worse on those pages: ranking loss. Content was overtaken, or those pages changed. Continue with `content-decay-audit` and `keyword-cannibalization-check`.
   - Concentrated, impressions near zero: the pages left the index, were redirected, or were noindexed. Continue with `technical-crawl-audit`.
   - Diffuse, position worse everywhere: site-wide. Either a Google core update or a site-wide change (template, robots, Core Web Vitals). Fetch Google's Search Status Dashboard with your browsing and check whether an update started within a few days of the inflection.
   - Diffuse, impressions down, position flat: demand. Seasonality or the market. Compare against the same weeks last year before calling it a problem.
   - Impressions flat or up, CTR down: the search results page changed shape. AI overviews or new features are absorbing clicks. Continue with `ctr-underperformers`.
   - Brand queries down: brand demand or a tracking issue, not SEO. Split brand from non-brand with a `filters` entry on `query` and report the two separately.

5. **Report.**
   Lead with the inflection date, the magnitude (clicks per week before and after), and whether the loss is concentrated or diffuse. Give the diagnosed cause with the evidence pattern that supports it, list the causes you ruled out and why, and name the one playbook to run next. One cause, one next step.

## Gotchas

- **The last 3 days always look like a drop.** Search Console has not finished processing them. End every window 3 days back or the timeline will show a cliff that does not exist.
- **An update is not the cause unless the dates line up.** A drop that started two weeks before an update was not caused by it. Correlation with a dated event is the strongest evidence this playbook can offer; it is still correlation.
- **Migrations reset everything.** A move from a URL-prefix property to a domain property, a domain change, or a redesign that changed URLs will look like catastrophic loss on one property and growth on another. Ask whether anything moved.
- **Sixteen weeks is not enough for seasonal businesses.** If the drop lines up with the same weeks last year, it is seasonality. Pull the year-ago window before diagnosing.
- **Total impressions can rise while clicks fall.** That is not contradictory; it is the signature of a results page that answers the query without a click.

---

## Skill: Keyword cannibalization check

- Slug: `keyword-cannibalization-check`
- Category: SEO
- Works with: google_search_console
- Page: https://sequel.sh/skills/keyword-cannibalization-check

Find out whether two or more of your own pages or blog posts cover the same ground and compete for the same keyword, cannibalizing each other and splitting clicks, and whether to merge them, differentiate them or leave them alone.

Cannibalization is when two or more of your own URLs rank for the same query. Google picks one, often inconsistently, and neither page performs as well as a single consolidated page would.

## Steps

1. **Pull query + page pairs.**
   `google_search_console.query` with `dimensions: ["query", "page"]`, a 90-day window ending 3 days ago, `rowLimit: 1000`. A long window matters here, since flip-flopping between URLs only shows up over time.

2. **Group by query.**
   Keep queries returning two or more distinct pages with meaningful impressions (set a floor, e.g. each page above 50 impressions, and state it).

3. **Rank the severity.** Worst first:
   - Both pages rank on page one for the same query → clearest, most costly case.
   - Pages alternate across the period, neither stable → classic ambiguity signal.
   - One page dominant, another with trivial impressions → usually benign; don't over-report it.

4. **Classify the intent overlap.** Fetch what each competing page primarily ranks for overall. Two pages sharing one query but otherwise serving different query sets is normal overlap, not cannibalization. Only pages whose overall query sets substantially overlap need consolidating.

5. **Report.**
   Per conflict: the query, competing URLs, impressions/clicks/position for each, and a recommendation: consolidate and redirect, differentiate the targeting, or leave alone. Include "leave alone" cases explicitly so the reader knows they were considered.

## Gotchas

- **Overlap is not automatically a problem.** A category page and a blog post both surfacing for one broad term is often fine. Recommending a merge on thin evidence destroys working pages.
- **Never recommend a redirect without checking the traffic on both URLs.** Redirecting the page that actually earns the clicks is a self-inflicted outage.
- **Search Console attributes to the canonical URL.** If two URLs share a canonical you'll see one page, not two. Absence of cannibalization here doesn't prove absence in the index.
- **This needs a long window.** Over 7 days, normal fluctuation looks like cannibalization.

---

## Skill: Measure traffic from AI assistants

- Slug: `ai-referral-traffic`
- Category: SEO
- Works with: google_analytics, google_search_console
- Page: https://sequel.sh/skills/ai-referral-traffic

Measure how many sessions ChatGPT, Perplexity, Claude, Gemini, Copilot and other AI assistants send to the site in GA4, which pages they land on, whether those visitors convert, and how the trend compares with Google organic.

AI assistants are a small but fast-growing referral channel that most analytics setups lump into "referral" and never look at. The visitors tend to arrive with a specific question already half-answered, which makes them unusually valuable to understand. This playbook isolates them.

## Steps

1. **Find the AI sources.**
   `google_analytics.run_report` with `dimensions: ["sessionSource", "sessionMedium"]`, `metrics: ["sessions", "engagementRate", "conversions"]` over 90 days, `limit: 5000`. Keep two sets. First, rows whose medium is `ai-assistant`: since May 2026 GA4 assigns that medium (and the "AI Assistant" default channel group) to sessions from assistant referrers it recognises. Second, rows whose source matches this list, which covers data from before that change and assistants Google has not added: `chatgpt.com`, `chat.openai.com`, `perplexity.ai`, `claude.ai`, `gemini.google.com`, `copilot.microsoft.com`, `you.com`, `poe.com`, `meta.ai`, `phind.com`, `kagi.com`. ChatGPT often appends `utm_source=chatgpt.com`, so the source may carry medium `referral` or a UTM medium; keep both. Deduplicate: a row is counted once even if it matches on medium and on source.

2. **Pull the trend.**
   `google_analytics.run_report` with `dimensions: ["date", "sessionSource"]`, `metrics: ["sessions"]`, and a `dimensionFilter` on `sessionSource` with `matchType: "FULL_REGEXP"` and a value like `.*(chatgpt|openai|perplexity|claude\.ai|gemini\.google|copilot\.microsoft).*`. Aggregate to weeks.

3. **Pull the landing pages.**
   Same filter with `dimensions: ["landingPage", "sessionSource"]`, `metrics: ["sessions", "engagementRate", "conversions"]`. These are the pages the assistants are citing.

4. **Compare with Google organic.**
   Run the landing-page report again with `dimensionFilter` on `sessionMedium` `EXACT` `organic`. Compare total sessions, engagement rate and conversion rate. Report AI referrals as a share of organic sessions so the size is in context.

5. **Cross-reference with Search Console.**
   `google_search_console.query` with `dimensions: ["page"]`, 90 days ending 3 days ago, `rowLimit: 500`. Pages the assistants cite that rank weakly in Google are a distinct finding: the content works for answer engines and could work for search with the right targeting. Pages strong in both are the ones to protect.

6. **Report.**
   Sessions by assistant with the weekly trend, the top ten AI-cited landing pages with engagement and conversions, the comparison with Google organic, and the cross-reference findings. Continue with `ai-crawler-access-check` if a major assistant sends nothing, since a block is the first thing to rule out.

## Gotchas

- **This undercounts.** Referrers are often stripped, in-app browsers may send none, and answers that are read without a click send nothing at all. The number is a floor.
- **Google AI Overviews are invisible here.** Clicks from them arrive as `google / organic`. Search Console does not separate them either.
- **Source strings drift.** `chat.openai.com` became `chatgpt.com`; new assistants appear. Check the raw source list in step 1 for anything that looks like an assistant before trusting the regex or the `ai-assistant` medium, which only covers the assistants Google recognises.
- **Small numbers swing.** A channel with 40 sessions a week doubles and halves on noise. Report trends over weeks, not days.
- **GA4 conversions are whatever key events are configured.** Say which ones you counted.

---

## Skill: Recover lost backlinks

- Slug: `lost-backlink-recovery`
- Category: SEO
- Works with: ahrefs
- Page: https://sequel.sh/skills/lost-backlink-recovery

Find the backlinks a site recently lost, work out why each one disappeared (the linked page was removed, the link was edited out, a redirect broke), and build a prioritised list of links worth recovering with a fix on your side or a short outreach.

Most lost backlinks are lost for reasons that have nothing to do with the linking site changing its mind: the page they pointed at was deleted or moved on your side. Those come back with a redirect and no email. This playbook separates those from the ones that need outreach and the ones that are gone for good.

## Steps

1. **Pull the backlinks with recency.**
   `ahrefs.backlinks` with the site as `target`, `mode: "subdomains"`, `limit: 1000`. Each row has the source URL, the target URL on the site, anchor text, the source domain rating and traffic, first and last seen dates, and link type.

2. **Define lost.**
   A link whose `last_seen` is older than 60 days is treated as lost; within 30 days is alive; between is uncertain. State the cutoff. Ahrefs recrawls busy pages often and quiet pages rarely, so a stale date on a tiny site is weaker evidence than one on a large site.

3. **Prioritise.**
   Sort the lost list by source domain rating, then by source traffic. The top 20 to 30 are worth diagnosing individually; the rest get a summary.

4. **Diagnose each of the top rows with your own browsing.**
   Fetch both the source URL and the target URL.
   - Target URL returns 404 or 410, or redirects to the homepage: the loss is on your side. Restore the page or 301 it to the closest live equivalent. Highest return, no outreach.
   - Source URL returns 404: their page is gone. Usually unrecoverable; note it and move on.
   - Source URL is live and the link is gone, or now points at a competitor: outreach. Note what replaced it, since that is the pitch.
   - Source URL is live and the link is still there: Ahrefs is stale, not the link. Mark it as a false positive and do not count it.

5. **Report.**
   A table of lost links sorted by source DR: source URL, target URL, anchor, DR, source traffic, last seen, diagnosis, action. Then two lists: your URLs to restore or redirect (with the number of links each recovers), and the outreach list with the specific page and the reason the link was worth having.

## Gotchas

- **Stale is not lost.** Step 4 exists because a large share of "lost" links in any index are simply not recrawled yet. Report the false-positive rate you found.
- **Redirect to the equivalent page, not the homepage.** A homepage redirect keeps the link but loses the relevance, and Google treats mass homepage redirects as soft 404s.
- **The 1000-row cap is the top slice on large sites.** Say so, and offer to rerun with `mode: "prefix"` on a section of the site.
- **Nofollow links are worth recovering too** when the source has traffic; they send visitors even if they pass no authority.
- **Do not promise a ranking change from recovered links.** Report what was recovered and what to watch.

---

## Skill: Review index coverage of important pages

- Slug: `index-coverage-review`
- Category: SEO
- Works with: google_search_console
- Page: https://sequel.sh/skills/index-coverage-review

Estimate how well the pages that matter are indexed when the Coverage report is not available through the API: combine Search Console page data with the sitemap and URL inspection of a prioritised sample to find pages Google has dropped, pages with the wrong canonical, and sitemap URLs that never earn an impression.

Search Console's own coverage report is not available through the API, but two things are: the pages that earn impressions (which are therefore indexed) and the URL Inspection API for anything specific. Put together with the sitemap, that is enough to find the pages that should be indexed and are not.

## Steps

1. **Pull the pages Google is showing.**
   `google_search_console.query` with `dimensions: ["page"]`, 90 days ending 3 days ago, `rowLimit: 1000`. Every page here has been indexed at some point in the window.

2. **Pull the sitemap picture.**
   `google_search_console.list_sitemaps` for submitted and indexed counts per sitemap; the gap is the first coverage number. Then fetch the sitemap XML with your own browsing to get the full URL list (call `list_sitemaps` again with `sitemapIndex` set if there is an index).

3. **Find the two mismatches.**
   - Sitemap URLs with zero impressions in 90 days: possibly not indexed, or indexed with no demand. Rank them by how much they matter: product, pricing, docs and top-level guides first, tag pages and archives last. Ask if the ranking is not obvious.
   - Pages with impressions that are not in the sitemap: indexed, but the sitemap is incomplete. A list to add.

4. **Inspect a prioritised sample.**
   `google_search_console.inspect_url` with up to ten URLs per call: the five highest-priority zero-impression URLs, plus five of the top-traffic pages to catch canonical drift on the pages that matter most. Run a second call if the user wants more, and say the API is quota-limited so this is a sample.

5. **Classify the inspected rows.**
   Indexed (verdict PASS, coverage submitted and indexed), not indexed by choice (noindex, canonical elsewhere), not indexed by problem (crawled or discovered but not indexed, blocked, fetch error), and canonical mismatch (Google canonical differs from user canonical). Count each.

6. **Report.**
   The sitemap submitted-vs-indexed table; the count and top twenty of zero-impression sitemap URLs; the count and top twenty of indexed pages missing from the sitemap; the inspected sample with its classification; and a short list of URLs to hand to `page-not-indexed-debug` one at a time.

## Gotchas

- **Zero impressions is not proof of not indexed.** A page can be indexed and never shown. Inspection is the only confirmation, which is why the sample exists.
- **The sitemap report's indexed count is Google's own and lags.** Treat it as directional.
- **Search Console page rows are capped and filtered.** A large site will not return every indexed page in 1000 rows. Raise the limit or scope with a `page` filter per section.
- **Inspect the winners too.** Canonical drift on a top page (Google picking a different URL) costs more than a hundred unindexed tag pages.
- **Search Console lags 2 to 3 days.** End the window 3 days back.

---

## Skill: Run a technical SEO audit

- Slug: `technical-crawl-audit`
- Category: SEO
- Works with: google_search_console
- Page: https://sequel.sh/skills/technical-crawl-audit

Check the pages that matter for status codes, canonicals, noindex tags, titles, headings and sitemap coverage, prioritised by the Search Console traffic each page earns, so technical fixes go where they protect the most visits.

A technical audit that checks ten thousand URLs equally produces a list nobody acts on. This one starts from the pages Search Console says earn the traffic, checks those first, and reports every finding with the clicks at stake next to it.

Sequel supplies the traffic data. The page checks use your own browsing: fetch the URLs yourself. If you cannot fetch pages, run steps 1 and 3, report the sitemap coverage findings, and say the page-level checks were skipped.

## Steps

1. **Build the priority list.**
   `google_search_console.query` with `dimensions: ["page"]`, a 90-day window ending 3 days ago, `rowLimit: 1000`. Sorted by clicks, this is the order in which every check below is run and reported.

2. **Fetch robots.txt and the sitemap.**
   Read `/robots.txt` for `Sitemap:` lines and crawl rules, then fetch the sitemap (or sitemap index and its children) and collect every URL it lists.

3. **Check sitemap coverage both ways.**
   Search Console pages with clicks that are absent from the sitemap: Google found them by other means, and the sitemap is incomplete. Sitemap URLs with zero impressions in 90 days: either not indexed or not worth listing. Report both lists with counts.

4. **Check the top pages.**
   Take the top 50 by clicks (say how many you did). For each, fetch and record: final status code and the number of redirect hops to reach it; the canonical URL and whether it points at itself; any `noindex` in meta robots or the `X-Robots-Tag` header; the title and its length; the meta description and its length; the number of H1 elements; `hreflang` and `lang` where present. Use `sequel_workbench` to run the fetches in a loop if you can execute code; otherwise fetch sequentially and lower the count.

5. **Grade the findings.**
   - **Critical**: a page with clicks that returns 4xx or 5xx; a page with clicks carrying `noindex`; a page with clicks whose canonical points at a different URL.
   - **High**: a redirect chain on a page with clicks; a missing or duplicated title among the top pages; sitemap entries that redirect or 404.
   - **Medium**: missing descriptions, multiple H1s, titles over 60 characters, descriptions over 155.
   For each finding, attach the clicks of the affected page from step 1.

6. **Report.**
   Findings first, grouped by severity, each row: URL, clicks at stake, the issue, the fix. Then the sitemap coverage summary. Then one sentence on what was not checked (Core Web Vitals, structured data, rendering) so the reader knows the scope. Continue with `redirect-chain-audit` if chains were found and with `metadata-audit` if titles were the main problem.

## Gotchas

- **A noindex or an off-page canonical may be deliberate.** Report it as an observation with the clicks at stake and ask; do not call it a bug until the user confirms it was unintended.
- **Fetch as a normal browser would.** Some sites serve different heads to bots. If a canonical or robots tag is injected by JavaScript, your fetch will not see it; say so if the HTML looks thin.
- **Do not crawl the whole site.** Fifty to a few hundred pages, chosen by traffic, is the audit. Thousands of fetches is a load test.
- **Sitemaps should list only canonical, indexable, 200 URLs.** Anything else in one is a finding by itself.
- **Search Console lags 2 to 3 days.** End the window 3 days back.

---

## Skill: Split brand vs non-brand search traffic

- Slug: `brand-vs-nonbrand-split`
- Category: SEO
- Works with: google_search_console
- Page: https://sequel.sh/skills/brand-vs-nonbrand-split

Separate branded searches from non-brand organic traffic to see whether SEO is actually growing, how dependent the site is on people already searching for the brand, and which trend the headline Search Console numbers are hiding.

Headline organic traffic blends two different things: people who already knew the brand and typed it in, and people who found the site by searching for a problem. Only the second one is SEO. A site whose total clicks are growing while non-brand clicks are flat is not gaining ground in search; it is gaining ground in brand awareness.

## Steps

1. **Get the brand terms.**
   Ask the user for the brand name, product names and common misspellings if they are not obvious. Build one case-insensitive regex from them, for example `(?i)acme|acmehq|ackme`. Search Console regex is RE2 syntax.

2. **Pull the two trends.**
   Two calls to `google_search_console.query` with `dimensions: ["date"]` over the last 16 weeks ending 3 days ago. One with `filters: [{ dimension: "query", operator: "includingRegex", expression: "<brand regex>" }]`, one with `operator: "excludingRegex"` and the same expression. Also run the unfiltered call so you have the true total.

3. **Pull the page view.**
   Repeat both filtered calls with `dimensions: ["page"]` over the last 28 days. This shows which pages carry non-brand traffic, which is the list of pages doing real SEO work.

4. **Compute share and trend.**
   Non-brand share of clicks, non-brand week-over-week trend, brand trend. Compare the two slopes. Non-brand growth is the SEO KPI; report it as such.

5. **Report.**
   Two trend lines (brand, non-brand), the current non-brand share, the top ten non-brand pages and queries, and one sentence on what the headline number is hiding. If the unattributed gap from the first gotcha is large, say so and give its size.

## Gotchas

- **Filtered totals do not sum to the unfiltered total.** Search Console omits anonymised low-volume queries from any query-filtered result, so brand plus non-brand will come in below the true total. Report the difference as "unattributed", never fold it into non-brand.
- **Brand plus a modifier is still brand.** "acme pricing" and "acme vs competitor" are branded queries. The regex catches them; do not hand-correct them into non-brand.
- **Homepage traffic is mostly brand.** If non-brand clicks land heavily on the homepage, check the queries; a generic homepage rarely ranks non-brand and this usually means the regex missed a brand variant.
- **Search Console lags 2 to 3 days.** End every window 3 days back.

---

## Skill: Tear down a competitor's top pages

- Slug: `competitor-top-pages-teardown`
- Category: SEO
- Works with: ahrefs, google_search_console
- Page: https://sequel.sh/skills/competitor-top-pages-teardown

See which of a competitor's pages earn the most organic traffic, what each one ranks for, which formats and topics keep winning for them, and which of those the site has no answer to yet.

A competitor's top pages are a free content strategy: the topics and formats that already work in this market, ranked by how much they earn. The job is to read the pattern, not to copy the pages.

## Steps

1. **Confirm the competitor.**
   If the user has not named one, ask. Search competitors (the sites that outrank you) are often not the business competitors the user has in mind.

2. **Pull their top pages.**
   `ahrefs.top_pages` with the competitor's domain as `target`, `limit: 100`, `order_by: "traffic:desc"`. Each row has the URL, estimated traffic and value, ranking keyword count, and the lead keyword with its volume.

3. **Classify each page.**
   From the URL and lead keyword, assign a format (guide, comparison, listicle, tool or calculator, template, glossary entry, product page, pricing, case study) and a topic. Fetch their top ten pages with your browsing to confirm the format and note structure, length, and whether the page is updated regularly. State that formats below the top ten were inferred from URLs.

4. **Find the pattern.**
   Aggregate traffic and value by format and by topic. The finding is usually one sentence: "sixty percent of their organic value comes from comparison pages on topic X". That sentence is what the user needs.

5. **Map their pages against the site's coverage.**
   `google_search_console.query` with `dimensions: ["query", "page"]` over 90 days ending 3 days ago, `rowLimit: 5000`. For each of the competitor's top 25 pages, search this result for their lead keyword. Three outcomes: the site ranks for it under position 10 (covered), the site ranks above 20 or gets impressions on an unrelated page (weak answer), or the keyword is absent (no answer).

6. **Compare the shape, not just the size.**
   `ahrefs.top_pages` for the user's own domain with the same settings. Compare how concentrated traffic is in the top ten pages for each site, and how the format mix differs.

7. **Report.**
   Their top 20 pages (URL, traffic, value, lead keyword, volume, format, and the site's answer: covered, weak, or none). The format and topic breakdown with the one-sentence pattern. Then five recommended pages to build, each with the format that wins for the competitor and the query it targets. Say which of their pages you are not recommending an answer to, and why.

## Gotchas

- **Ahrefs traffic and value are modelled.** They rank pages against each other well; they do not measure anyone's revenue. The user's own Search Console numbers are measured; the competitor's are not.
- **Their homepage and brand pages inflate the list.** Strip pages whose lead keyword is their brand before computing the format mix.
- **Copying a page that ranks does not make yours rank.** The recommendation is the format and the topic, plus what would make the page better than theirs.
- **Formats are market-specific.** What wins in developer tools loses in consumer retail. Read the pattern from this competitor, not from a general rule.
- **Lead keyword is one of many.** A page with 400 ranking keywords is not "about" its lead keyword alone. Check `ahrefs.organic_keywords` for the page URL before writing a brief around it.
- **For the links behind those pages**, continue with `competitor-new-backlinks`, which shows who is linking to them and how each link was earned.

---

## Skill: Trace organic search to revenue and pipeline

- Slug: `organic-traffic-to-revenue`
- Category: SEO
- Works with: google_search_console, google_analytics, hubspot, stripe, dodo_payments
- Page: https://sequel.sh/skills/organic-traffic-to-revenue

Find which keywords and pages bring in paying customers: follow organic search all the way to purchases in GA4 or contacts and closed deals in HubSpot, so SEO is measured in revenue and pipeline instead of clicks.

This is the question a dedicated SEO tool cannot answer, because the answer lives in three systems: what people searched for (Search Console), what they did on the site (GA4), and what they paid or became (HubSpot, Stripe). Joining them is what makes an SEO number a business number.

## Steps

1. **Pick the revenue path.**
   Self-serve or ecommerce: revenue is in GA4 (path A). Sales-led B2B: pipeline is in HubSpot (path B). Both can be true; run both and keep them separate. Ask which applies if it is not obvious.

2. **Pull the organic side.**
   `google_search_console.query` with `dimensions: ["page"]` over 90 days ending 3 days ago, `rowLimit: 500`, and again with `dimensions: ["query", "page"]`, `rowLimit: 5000`. Ninety days, because the path from search to money is longer than the path from search to click.

3. **Path A: revenue in GA4.**
   `google_analytics.run_report` with `dimensions: ["landingPage"]`, `metrics: ["sessions", "conversions", "totalRevenue", "ecommercePurchases"]`, the same dates, `dimensionFilter` on `sessionMedium` `EXACT` `organic`. Join to Search Console on the normalised path. Compute revenue per organic click per page.

4. **Path B: pipeline in HubSpot.**
   First `hubspot.list_properties` for contacts and for deals to find the analytics source and first-page fields. They are usually `hs_analytics_source`, `hs_analytics_first_url` and `hs_analytics_source_data_1`, but accounts differ; do not hardcode. Then:
   - `hubspot.search_contacts` with a `filterGroups` entry for the source property `EQ` `ORGANIC_SEARCH` and `createdate` within the window, requesting `properties` for email, create date and first URL. Group contacts by the path of their first URL and join to Search Console pages. This gives leads per organic page.
   - `hubspot.list_pipelines` for deals to learn which stage ids mean closed won. Then `hubspot.search_deals` filtered to the organic source and closed won within the window, requesting amount, close date and the first-URL property if deals carry one. This gives revenue from organic at the account level even when deal-to-page attribution is missing.
   Page through with `after` until exhausted; a partial pull silently understates everything.

5. **Stripe or Dodo Payments, only if attribution was captured.**
   A payment provider has no idea where a customer came from unless the checkout stored it. Ask whether UTM or referrer values are written to customer, subscription or payment metadata. If they are, `stripe.search` with a metadata query on the organic value gives revenue directly. Dodo returns `metadata` on every row of `dodo_payments.list_payments` but cannot filter on it, so pull the window with `status: "succeeded"` and filter the metadata column in the workbench. If nothing was captured, skip this step and say so rather than guessing.

6. **Attribute queries.**
   For the top revenue pages, take their query rows from step 2 and report the queries as the demand behind the revenue. This is query-to-page-to-revenue, not query-to-revenue; keep that distinction in the write-up.

7. **Report.**
   Top ten organic pages by revenue or pipeline: page, clicks, sessions, conversions or contacts, revenue or closed-won amount, revenue per click, and the top three queries behind it. Then the share of total revenue or pipeline that organic accounts for, pages with real traffic and no revenue, and the one or two queries most worth defending. Keep GA4 revenue and HubSpot revenue in separate columns; never add them.

## Gotchas

- **Different systems attribute differently.** HubSpot's source is first touch by its own tracking cookie; GA4's is session-scoped and defaults to last non-direct click. Present each as what it is. Adding them double counts.
- **GA4 revenue requires ecommerce events.** If `totalRevenue` is zero everywhere, the site is not sending purchase events; that is a tracking gap, not zero revenue.
- **HubSpot's ORGANIC_SEARCH is HubSpot's classification**, based on referrer, not on Search Console. It includes all search engines and misses visitors whose referrer was stripped.
- **Sales cycles outlast the window.** A deal closed this quarter came from a search last quarter. Use the contact's create date for lead attribution and the close date for revenue, and say the windows differ.
- **Stripe amounts are in minor units** (cents). Divide by 100 once, and report per currency.
- **Join on the path.** Strip origin, query string and trailing slash from Search Console, GA4 and HubSpot URLs before joining, and report the unmatched share.

---

## Skill: Turn search demand into a content plan

- Slug: `query-clusters-to-content-plan`
- Category: SEO
- Works with: google_search_console, ahrefs
- Page: https://sequel.sh/skills/query-clusters-to-content-plan

Find content gaps and plan what to write next from Search Console data: group the queries the site already gets impressions for into topic clusters, map each cluster to the page that serves it, and surface clusters with real demand but no dedicated page.

The best content plan comes from queries Google is already testing the site against. Every impression is Google saying "this site might be relevant here". Clusters of those impressions that no page is built for are the gaps worth writing to, and they come with proof of demand attached.

## Steps

1. **Pull the query inventory.**
   `google_search_console.query` with `dimensions: ["query", "page"]`, a 90-day window ending 3 days ago, `rowLimit: 5000`. Strip branded queries. Keep rows with at least 30 impressions and state the floor.

2. **Cluster the queries.**
   Group by shared head term and intent. Do this in `sequel_workbench` or by inspection; either way, state the method. Label each cluster with a representative query and an intent: informational, commercial investigation, transactional, or navigational.

3. **Score each cluster.**
   Total impressions, total clicks, impression-weighted average position, the distinct pages that received impressions for it, and which single page received the most.

4. **Classify the clusters.**
   - **Gap**: meaningful impressions, average position above 20, impressions spread thinly across pages that were not written for the topic. No page serves this; a dedicated one probably would.
   - **Cannibalised**: several pages each receiving a solid share. Continue with `keyword-cannibalization-check` rather than adding another page.
   - **Under-clicked**: one page, good position, low CTR. Continue with `ctr-underperformers`.
   - **Served**: one page, position under 10. Leave it alone.

5. **Size the top gaps (if Ahrefs is connected).**
   Pass the representative queries of the top gap clusters to `ahrefs.keywords_overview` for volume, difficulty and CPC. Drop gaps whose difficulty is far above what the site already ranks for. High CPC marks commercial intent.

6. **Report.**
   A cluster table (cluster, intent, impressions, clicks, position, serving page, classification), then briefs for the top five gaps: working title, the queries to target, the intent to satisfy, and which existing page should link to the new one. Explicitly list the clusters you are recommending nothing for.

## Gotchas

- **Plan clusters, not keywords.** One page per cluster. A page per query is how cannibalization starts.
- **Do not build a page for a cluster an existing page already serves at position under 10.** Improve that page instead; a new one splits the signal.
- **Search Console omits the anonymised long tail.** The inventory is a sample of demand, not all of it. Clusters are more robust to this than individual queries, which is another reason to cluster.
- **Ahrefs numbers are estimates.** Use them to rank gaps against each other, not as a forecast of traffic.
- **Intent decides the format.** An informational cluster wants a guide; a commercial one wants a comparison; a transactional one wants a product or landing page. Say which in each brief.

---

## Skill: Acquisition channel performance

- Slug: `acquisition-channel-performance`
- Category: Marketing
- Works with: google_analytics, google_search_console
- Page: https://sequel.sh/skills/acquisition-channel-performance

Compare traffic sources and marketing channels on volume, engagement and conversion rate to decide where to spend more and where to cut: which channel gives the best return, whether to shift budget between paid, organic, referral and email, and how each is trending.

The question behind this is always "where should we spend more?" Sessions alone can't answer it — a channel with half the traffic and triple the conversion rate is the better channel.

## Steps

1. **Pull channel-level performance.**
   `google_analytics.run_report` with `dimensions: ["sessionSourceMedium"]`, `metrics: ["sessions", "totalUsers", "newUsers", "engagementRate", "averageSessionDuration", "conversions"]` (add `"totalRevenue"` if the property tracks it), over a 30-day window, `limit: 50`.

2. **Compute per-channel efficiency.** Conversions ÷ sessions per channel. This ratio, not raw sessions, is what should drive the recommendation.

3. **Add a trend.**
   Re-run with `dimensions: ["date", "sessionSourceMedium"]` to see whether each channel is growing or shrinking. A small but fast-growing channel is often the real finding.

4. **Break organic down further (if Search Console is connected).**
   GA4 collapses all organic search into one bucket. `google_search_console.query` with `dimensions: ["query"]` splits it into branded vs non-branded — filter by whether the query contains the brand name. Branded organic mostly reflects demand you created elsewhere; non-branded is what SEO actually earned. Reporting them as one number consistently overstates SEO.

5. **Report.**
   One table sorted by conversions: channel, sessions, engagement rate, conversions, conversion rate, and 30-day trend. Then name the highest-efficiency channel, the biggest-volume channel, and where they differ — that difference is the recommendation.

## Gotchas

- **Never rank channels by sessions.** It always flatters whatever is cheapest to buy.
- **Direct traffic is a junk drawer.** It absorbs untagged campaigns, app opens, and lost referrers. A large direct share usually means a UTM tagging problem, not genuine type-in traffic — say so.
- **GA4 conversions depend on configured key events.** If conversions come back as zero everywhere, the property likely has none configured. Report that finding rather than concluding nothing converts.
- **Attribution is last-click by default.** Channels early in the journey (organic, social) get systematically undercredited. Flag this before recommending anyone cut spend.
- **Don't join GA4 organic sessions to Search Console clicks and expect them to match.** They count different things (sessions vs clicks) with different bot filtering. Trends are comparable; absolute numbers aren't.

---

## Skill: Lead source to revenue attribution

- Slug: `lead-source-to-revenue-attribution`
- Category: Marketing
- Works with: hubspot, stripe, dodo_payments, google_analytics
- Page: https://sequel.sh/skills/lead-source-to-revenue-attribution

Measure marketing ROI by source: trace channels and campaigns through to closed revenue by joining CRM deals with payment records, so revenue per lead and return on spend are known instead of stopping at lead counts.

Most channel reports stop at leads. The channel producing the most leads is frequently not the one producing the most revenue, and that gap is the entire point of this analysis.

## Steps

1. **Understand the CRM shape before querying it.**
   Call `hubspot.list_properties` for contacts and deals to find the actual source field — it may be `hs_analytics_source`, a custom property, or something team-specific. Call `hubspot.list_pipelines` for deals to learn which stage IDs mean "closed won". Do not hardcode either; they differ per account.

2. **Pull deals with their source and value.**
   `hubspot.search_deals` with `filterGroups` scoping to the period, requesting `properties` including deal name, amount, close date, stage, and the source property you identified. Page through with `after` until complete — a partial pull silently biases every number downstream.

3. **Pull the contacts behind those deals** via `hubspot.search_contacts` when you need the original acquisition source rather than the deal-level one. The deal source is often inherited or overwritten; the contact's first-touch source is usually closer to the truth.

4. **Reconcile against actual payments.**
   CRM "closed won" is a human-entered state; money received is not. Use `stripe.list_charges` or `stripe.list_invoices` (with `status: "paid"`), or `dodo_payments.list_payments` with `status: "succeeded"`, over the same window and compare totals. Where they diverge, trust the payment provider for revenue and report the discrepancy — it's usually a finding in its own right.

5. **Add top-of-funnel volume for context.**
   `google_analytics.run_report` with `dimensions: ["sessionSourceMedium"]` and `metrics: ["sessions", "conversions"]` shows how much traffic each channel needed to produce those deals.

6. **Report.**
   One row per source: sessions, leads, deals created, deals won, revenue, and revenue per lead. Sort by revenue. Then explicitly contrast the lead-volume ranking against the revenue ranking — where they disagree is the recommendation.

## Gotchas

- **Attribution windows.** A deal closing this month may come from a lead generated months ago. Attributing by close date credits whatever channel was running then. Prefer joining on the contact's original source and note the lag.
- **HubSpot source values are messy.** Free-text and legacy values proliferate. Normalize case and obvious duplicates before grouping, and say what you merged.
- **Stripe amounts are in the smallest currency unit** (cents). Divide by 100 before reporting, and don't sum across currencies without converting — if multiple currencies appear, report them separately.
- **Deleted or test deals** frequently sit in CRM data. Sanity-check the total against a known figure before presenting it.
- **Small numbers aren't signal.** A channel with 3 deals and one large contract will top a revenue-per-lead table. Report deal counts alongside so the reader can see the sample size.

---

## Skill: Signup funnel drop-off analysis

- Slug: `signup-funnel-drop-off`
- Category: Product Analytics
- Works with: posthog, mixpanel, amplitude
- Page: https://sequel.sh/skills/signup-funnel-drop-off

Find the step where the most users abandon signup or onboarding, and how that varies by segment.

Use whichever product analytics source is connected — the shape of the analysis is the same, only the call differs. Do not query all three and merge; they define sessions and users differently and the numbers will not reconcile.

## Steps

1. **Establish the real step sequence.**
   Don't assume it. List the available events first:
   - PostHog: `posthog.list_events`
   - Mixpanel: `mixpanel.list_events`
   - Amplitude: `amplitude.list_events`
   Confirm the ordered step names with the user if they're ambiguous. A funnel built on guessed event names produces confidently wrong output.

2. **Run the funnel.**
   - **Amplitude:** `amplitude.funnel_report` with the ordered `events` array and an explicit `conversion_window_seconds`.
   - **Mixpanel:** `mixpanel.list_funnels` first, then `mixpanel.funnel_report` with the `funnel_id`. Mixpanel funnels are saved objects — you generally cannot define an ad-hoc one here.
   - **PostHog:** `posthog.query` with a HogQL query over the events table, or `posthog.list_insights` to find an existing saved funnel.

3. **Compute step-to-step conversion, not just overall.** The largest single drop between adjacent steps is the finding. Overall conversion tells you there's a problem; step-to-step tells you where.

4. **Segment the worst step.**
   Re-run split by a meaningful dimension (`group_by` on Amplitude, `on` for Mixpanel, a GROUP BY in HogQL) — device, plan, signup source, or country. Drop-off is rarely uniform, and a step that's fine on desktop but collapses on mobile is a much sharper finding than the average.

5. **Report.**
   A step-by-step table: step name, users entering, users continuing, step conversion rate, drop-off count. Call out the worst step, quantify what fixing it to parity would be worth in absolute users, and note the segment where it's worst.

## Gotchas

- **The conversion window drives the result.** A 1-hour window and a 7-day window on the same funnel give very different answers. State the window you used; if the product has a long consideration cycle, a short window manufactures drop-off that isn't real.
- **Funnels are ordered.** Users completing steps out of order may be excluded entirely depending on the tool's mode. Check whether the funnel is strict or any-order before interpreting.
- **Instrumentation gaps look exactly like user drop-off.** If a step shows near-total loss, verify the event is actually firing before concluding the UI is broken.
- **`mixpanel.export_events` is expensive** — it streams raw events. Use `mixpanel.segmentation` or a saved funnel unless raw rows are genuinely required.
- **Don't compare funnel numbers across tools.** If two are connected, pick one, and say which.

---

## Skill: Subscription revenue health

- Slug: `subscription-revenue-health`
- Category: Revenue
- Works with: stripe, polar, dodo_payments
- Page: https://sequel.sh/skills/subscription-revenue-health

Track MRR movement, churn, subscriptions that failed to renew, failed payments and refunds to separate genuine growth from collection problems.

Headline MRR hides the mechanics. Growth built on new signups while existing customers churn is a very different business from growth built on retention, and involuntary churn (failed cards) is a fixable problem often misread as customers leaving.

## Steps

1. **Prefer pre-computed metrics when available.**
   If Polar is connected, `polar.get_metrics` returns MRR, ARR, and subscription counts as a time series — use it rather than recomputing from raw orders. Set `interval` to month for trend, day for a recent window.

2. **Pull the subscription base.**
   `stripe.list_subscriptions`, `polar.list_subscriptions` or `dodo_payments.list_subscriptions` filtered by status. Count active, trialing, past_due, and canceled separately. `past_due` is the number to watch — those are customers who intend to pay but whose payment failed. On Dodo the equivalent state is `on_hold`, and `cancel_at_next_billing_date: true` marks subscriptions that are still active but already lost.

3. **Compute the MRR movement breakdown.** Decompose the period's change into new, expansion, contraction, and churned MRR. A single net number is nearly useless; the composition is the analysis.

4. **Separate voluntary from involuntary churn.**
   Cross-reference cancellations against failed payments — `polar.list_payments` and `dodo_payments.list_payments` (with `status: "failed"`, which also returns the `error_code`) expose failed attempts directly; on Stripe, `past_due` and `unpaid` subscription statuses plus unpaid invoices serve the same purpose. Involuntary churn is a dunning and card-updater problem, not a product problem, and conflating them sends the team to fix the wrong thing.

5. **Net out refunds and disputes.**
   `polar.list_refunds` and `polar.list_disputes`, `dodo_payments.list_refunds` and `dodo_payments.list_disputes`, or `stripe.list_charges` filtered to refunded. Gross revenue that ignores refunds overstates the business, and a rising dispute rate is an early warning worth surfacing on its own.

6. **Report.**
   A monthly table: starting MRR, new, expansion, contraction, churn, ending MRR, plus active subscriber count and churn rate. Then flag the involuntary churn share and the refund rate.

## Gotchas

- **All three APIs return integer minor units** (cents, or the currency's smallest unit). Divide by 100 exactly once, and never sum across currencies without converting — report per currency if several appear.
- **Normalize billing intervals before summing.** Annual plans must be divided by 12 to contribute to MRR. Summing raw amounts across monthly and annual plans inflates MRR badly, and it's the single most common error here.
- **Trials aren't revenue.** Exclude `trialing` subscriptions from MRR; count them separately as pipeline.
- **`cancel_at_period_end` means still active.** Those customers are still paying and still counted in MRR until the period closes — but they're the leading indicator of next month's churn, so report them separately.
- **Paginate fully.** These list endpoints cap per page; a partial pull produces a confidently wrong MRR. Page until exhausted, or state the limit you applied.
- **If more than one billing provider is connected**, do not add them together without checking whether they bill the same customers. Ask which is authoritative.
