Get started

.md

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, 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, 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. 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.

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.

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.

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.

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.

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.