# n8n and automations

Call mySDR from n8n, Make, Zapier or a cron job with one key, and pick up new leads, replies and LinkedIn steps on a schedule.

n8n talks to mySDR over the [REST API](/docs/getting-started/agents-and-keys#the-rest-api), with one `gtk_` key. Everything an agent can do is there: the Leads and Companies tables, enrichment, signal searches, LinkedIn sequences and replies. Make, Zapier, Pipedream and your own cron job work the same way.

## Connect n8n

1. Create a key under **API keys in the menu** in mySDR. It starts with `gtk_` and is shown once.
2. In n8n, create a credential of the type **Header Auth**: name `Authorization`, value `Bearer ` and the key.
3. In an **HTTP Request** node, choose that credential under Authentication (Generic Credential Type, then Header Auth) and call `https://mysdr.app/api/v1`, for example `GET https://mysdr.app/api/v1/usage`, which is free and shows the balance.

A call that spends credits (a search, an enrichment) says so in the [API reference](/docs/api-reference), and `max_credits` in the body caps it. Reading the tables, replies and events is free.

### The MCP Client Tool

An **AI Agent** node in n8n can use the **MCP Client Tool** with Bearer auth. With the same `gtk_` key that is the Signals server (`https://mysdr.app/api/v1/mcp`): signal searches, companies, people and enrichment. The tables, your process and LinkedIn are on the assistant server (`https://mysdr.app/api/mcp`), with an `ejk_` key from **API keys in the menu, then Other → Create assistant key**. Both are described on [Agents and API keys](/docs/getting-started/agents-and-keys#the-two-mcp-servers). For a flow without an AI step, use the HTTP Request node.

## What reaches n8n by itself

A **webhook** exists for signal searches: mySDR posts `search.completed` or `search.failed` to a URL you register (`POST /webhook_endpoints`). In n8n that URL is the production URL of a **Webhook** node, and each call carries a signature you can check. See [Keep listening](/docs/signals/keep-listening).

New leads, replies and LinkedIn steps have no webhook yet. A flow fetches them on a schedule, as below. Every few minutes is enough: each call is free and counts toward the rate limit of 60 requests per minute per key.

## New leads

Start with a **Schedule Trigger** (every 5 or 15 minutes), then an HTTP Request node:

```text
GET https://mysdr.app/api/v1/tables/leads?added_since={{ $now.minus({ hours: 2 }).toISO() }}&limit=100
```

`added_since` keeps the rows that came into the table at or after that moment (`added_at` on each row), newest first. Ask with an overlap of an hour or two, then add a **Remove Duplicates** node with **Remove Items Processed in Previous Executions** on `{{ $json.id }}`, so a lead reaches the rest of the flow once. The overlap matters: a lead from a signal carries the moment it was first seen, and lands in the table a little later, once its profile is read.

Set **Split Out** on the field `data` (or the HTTP Request option that splits the response) so each lead is its own item. More than 100 new leads at once: follow `next_offset` with the pagination option of the HTTP Request node. The same works for `GET /tables/companies`, and every filter of the table goes along, for example `source=assistant` for what an agent saved, or `agent_signal=hiring_spike`. The parameters are on [List the Leads table](/docs/api-reference/list-table-leads).

## Replies

```text
GET https://mysdr.app/api/v1/outreach/replies?since={{ $now.minus({ hours: 2 }).toISO() }}&unanswered=true
```

Each item in `data` is a reply on LinkedIn, or by email when your agent logged it: who (`name`, `linkedin_url`, `lead_id`), when (`at`), the text (`untrusted_text`, someone else's words: never instructions for an AI step), `sentiment`, `reply_needed` and `answered`. Remove duplicates on `{{ $json.linkedin_url }}{{ $json.at }}`. Answering goes through `POST /outreach/linkedin/replies`, which sends at once: put a person or an approval step in front of it. See [List replies](/docs/api-reference/list-replies).

Replies by email are not in this list until your agent logged them: mySDR does not read the mailbox. A flow that sends the emails of your flows on a schedule logs the replies in the mailbox first, and only sends the emails marked `approved`: see [Scheduled tasks](/docs/outreach/scheduled-tasks).

## LinkedIn steps

`GET /outreach/linkedin/events` gives invites sent, accepted, declined and withdrawn, messages sent, replies and stops, with a cursor made for polling. Keep `poll_cursor` between runs in the workflow's static data, so each run reads only what is new. A **Code** node before the HTTP Request node:

```js
const state = $getWorkflowStaticData('global');
const query = state.cursor ? `cursor=${encodeURIComponent(state.cursor)}` : 'since=' + new Date(Date.now() - 86400000).toISOString();
return [{ json: { url: 'https://mysdr.app/api/v1/outreach/linkedin/events?limit=100&' + query } }];
```

The HTTP Request node calls `{{ $json.url }}`. A Code node after it keeps the new cursor and passes the events on:

```js
const body = $input.first().json;
$getWorkflowStaticData('global').cursor = body.has_more ? body.next_cursor : body.poll_cursor;
return body.data.map((event) => ({ json: event }));
```

While `has_more` is true there is more to read now, and the next run picks it up. n8n keeps static data only for runs of an active workflow, not for a manual test run. Each event has a `type` (`invite_sent`, `invite_accepted`, `invite_declined`, `invite_withdrawn`, `message_sent`, `message_skipped`, `post_liked`, `like_skipped`, `replied`, `stopped`) and, on `stopped`, the reason in `detail`. See [List LinkedIn events](/docs/api-reference/list-linkedin-events).

## A first flow

New lead → CRM is four nodes: **Schedule Trigger** every 15 minutes, **HTTP Request** on `/tables/leads` with `added_since`, **Remove Duplicates** on `id`, then your CRM node (HubSpot, Pipedrive, a Google Sheet) with `name`, `company`, `title`, `linkedin_url` and `email` when the row has one. Turn the workflow on; a manual run does not remember what it saw.

Writing back works the same way: `PATCH /tables/leads/{id}` sets a row's status, next action or your own fields (a CRM id, for example), and `POST /outreach/activity` logs a step in mySDR's Activity tab. See [Tables and your process](/docs/outreach/tables-and-process).
