Outreach

.md

Tables and your process

Save leads and companies, keep each row’s state, check before every email and log every step, so the user needs no spreadsheet.


An agent that finds its own companies and mails with its own tools keeps its whole process here, so the user needs no spreadsheet next to it and the agent needs no memory of its own between two runs. Everything is free, over REST with a gtk_ key and on the assistant server. The user sees it in the Leads and Companies tabs (the Status, Next action, Contact and Outreach columns, your own fields as columns, and the log of each row in its panel) and on the Activity tab: the whole log as a table, with what, the result, the source and who logged it.

Save rows#

A search result is not in the user’s tables until you save it, with a note why: save_leads (POST /tables/leads) for people and save_companies (POST /tables/companies) for companies. Saving the same person or company again merges: what you send wins, what you leave out keeps its value. remove_saved (DELETE /tables/leads/{id}) takes out what you saved.

Keep the state of each row#

Save the state in the same call, or change it later with update_row (PATCH /tables/companies/{id} or PATCH /tables/leads/{id}):

  • status: new, needs_decision when it waits for the user, snoozed until snooze_until, or archived, with a status_reason.
  • lead_status, next_action (when, and follow_up, recontact, rescan, approval or other), out_of_office_until.
  • email and email_status on a person, contact_points on a company (its general addresses and contact form).
  • fields: your own columns, each with the URL where you found it.

A company is the unit when you mail its general address; its people stay on Leads. A lead from the user's own signals (a number) takes the whole state too: PATCH on its number. Someone who is also a lead from the signals is one row in Leads, with your state on it; archiving, snoozing and the lead status you set land on Leads itself, so the user can undo them there.

What waits for the user: set status needs_decision with the reason (or a next_action of kind approval). The user sees Action required on that row and answers you in your conversation.

Before and after every email#

  • Before every email, and before every sequence, flow or campaign step (email_1, email_2, a LinkedIn step), ask check_before_sending (POST /outreach/check): it says no, with reason and message, for an excluded or bounced address, an archived, snoozed or waiting row (also a lead from the signals that the user archived or snoozed on Leads), not interested, out of office, a step that already went to that company (any address at its domain, anyone; a bounced email does not count), a reply you did not answer yet (replied, or positive_reply when the sentiment is positive), or a booked meeting (meeting_booked). The default is a sequence step, so a meeting or a reply blocks even without step. Pass purpose conversation only for a one-to-one mail in the same thread, without step or flow_step: then a meeting or a reply is a warning and the mail stays allowed. A neutral or negative reply still blocks a sequence step. Logging the meeting or the reply also takes the lead out of active sequences, and a queued automated step is not sent. Give the company by its domain when you can.
  • After every email, log it with log_outreach_activity (POST /outreach/activity), with step, template, thread_id, external_id and the text (required: without it nothing is logged): it shows in the mySDR Inbox next to the person's LinkedIn messages, and GET /outreach/activity?with_text=true (get_activity with with_text) gives it back before a follow-up. Log every reply, bounce, out-of-office, unsubscribe and referral the same way: a bounce or unsubscribe puts the address on the exclusions, an out-of-office with until (the day they are back) moves your next action and the next step of their running sequences to that day (without until nothing moves), a reply clears a planned follow-up.
  • Plan the next email in the next action: next_action with email (text, subject, step) on update_row. The user sees on the Templates tab what goes out to whom and when; GET /agenda gives the text back on its day, and logging email_sent with that step clears it.

Log every step#

Every research step (careers page found, open roles counted, headcount checked, address found) is a note on the company with source_url, the page where you found it: the result goes in a field or contact point, the step and its source in the log. A note without a company, person or address is your work log: a run you finished, a source you tried, a credit budget you reached, a decision the user made. A step of your own that is none of the types (a call, a voicemail, a LinkedIn message you sent yourself, a visit) is a note with action (call, linkedin_message) and action_label: always the same key, so the user can filter and count them on the Activity tab.

The agenda#

At the start of a session, read get_agenda (GET /agenda): the follow-ups that are due (with the email you planned for them), the snoozes that end, who is back from out of office, and today's counts (emails, replies, meetings, bounces, unsubscribes, not interested, out of office, referrals) and the bounce rate of the last 7 days. GET /outreach/campaigns counts the same per campaign, with the reply and bounce rate.

Pipeline, team and campaigns#

  • The team's own pipeline: set_stages (POST /stages) sets the organization's stages in order ("New lead", "MQL", "SQL", "Demo booked"); agree them with the user first. Put each row in one with stage: next to status, never instead of it, and a change is logged. list_stages counts the rows per stage, ?stage= filters the table.
  • Owners: a row can carry an owner (me, or a member of the workspace from list_team, GET /team), and ?owner=me on the tables and GET /agenda?owner=me read only theirs; the user sees the Owner column. A plan has one user per workspace, so list_team usually lists only that user.
  • Campaigns without LinkedIn: create_campaign (POST /outreach/campaigns) makes one for an email pilot; put rows in it with campaign_id. The same name returns the same campaign.

Exclusions#

list_exclusions and add_exclusions (GET and POST /exclusions): customers, trials, test users, former customers, the sales pipeline, competitors, each with the source it came from (required). A person who gets the lead status not_interested is added by itself, with their profile and address. You keep this list: the user reads it on the Suppression list page but cannot change it there, so when they tell you a company or person must not be contacted, add it. They also see it as Do not contact on that row in Leads and Companies (do_not_contact on GET /tables/*, ?do_not_contact=true lists them); a bounced address only blocks email and is not marked.

Archive rules. The user can turn on rules on Leads that archive leads for them every day: when they said no, when the outreach ended without a reply for a number of days, when their email bounced, when they are on the exclusions, or when they stayed New with no new signal for a number of days. The rules that are on are in archive_rules on GET /tables/leads (and list_table); a lead a rule archived carries status_reason Archive rule: …. A lead the user or you restore (status new) is never archived by a rule again. Only the user changes the rules.

An exclusion is a domain, an address, a company name or one person's LinkedIn profile (linkedin_url): for someone who opts out of every channel, add their profile, because an address only marks a row that carries it. The response says which rows each exclusion marks (marks) and warns when an address marks none. A new exclusion also stops the LinkedIn sequences it hits. Search People leaves out people at an excluded company and does not charge them.

Keep your CRM in sync#

mySDR has no CRM connection of its own, and it does not need to be the user's source of truth. When the user keeps their pipeline in a CRM (HubSpot, Notion, Pipedrive …), the agent connects the two with its own connector, for example every few minutes:

  • From mySDR to the CRM. get_outreach_events (GET /outreach/linkedin/events) with the poll_cursor of the last read gives only what is new on LinkedIn: invites sent and accepted, messages, replies and stops, each with an id to write once. get_activity (GET /outreach/activity?since=) gives the log since a moment, and list_table (GET /tables/leads) each row's lifecycle and state.
  • From the CRM to mySDR. Check the CRM before you contact someone and note it on the row as the field hubspot, with source_url the record and the reason in the note. Use one of these values, so the user can filter the column: contact (a contact in the CRM, with nothing below going on), customer (they or their company are a customer), open_deal (there is an open deal), closed_lost (a deal with them was lost), opted_out (they opted out or asked not to be contacted), ae_active (an account executive is working them), company_only (their company is in the CRM, they are not), not_in_crm (neither they nor their company are in the CRM).
  • Who never gets a message. Customers who must never be approached, and people who opted out, go on the exclusions: mySDR then refuses them.

Signed webhooks exist only for signal searches (Keep listening): outreach and the tables are read this way.

Read it back#

list_table (GET /tables/leads and GET /tables/companies) gives the table as the user sees it: total is every row in it, with the state of every row and its lifecycle (the Status column: new, scheduled, contacted, replied, interested, meeting, later, no_reply, not_interested or archived, derived from status, lead_status and the outreach; a company shows its most advanced lead). Filter on lifecycle (open = new to meeting), status, due, campaign and your own fields (field.fte.gte=200, field.ats=recruitee, field.open_roles.exists=true). get_table_row (GET /tables/leads/{id}) gives one row with its log, and for a lead with a flow the texts prepared for them in flow_texts (per step: the text, the subject of an email or InMail, and whether it is ready); a list gives only flow_texts_missing. get_activity (GET /outreach/activity) gives the log of a row, an address, a campaign or the whole organization, your work log included, and action=call only your calls.

Dashboards the user asks for#

When the user asks for a dashboard ("show me my Q4 pipeline every morning"), build it with save_dashboard (POST /dashboards): it shows as its own tab on the Dashboard in mySDR, and refreshes by itself. The tabs of the Dashboard are only dashboards: mySDR builds the first one itself from the first data (Overview, key mysdr_overview), and everything after it is built by you. You describe it, mySDR draws it from the live tables and the activity log, so it never goes stale and needs no code:

  • a metric: how many rows match the filters of list_table ({"lifecycle": ["meeting"]}), or how many lines of the log in a period; divide_by makes it a percentage (a reply rate), compare adds the period before;
  • a chart: rows per group_by (status, stage, owner, a field such as field.hubspot), or the log per day or week;
  • a table: rows with the columns and sort you pick;
  • a note: your own text, such as the focus of the week or how a LinkedIn sequence runs;
  • LinkedIn limits (linkedin_limits): the user's invites today and in the last 7 days and new sequences today, each used of its limit, with a pause and the warm-up, live.

When the user asks about their LinkedIn outreach, get_outreach_status and a start of start_linkedin_sequence carry a dashboard_offer: a dashboard with the limits, the people and how the sequence runs, ready to save. Ask the user whether they want it as a tab, and save it only after a yes. Saving companies works the same way: when save_companies (or POST /tables/companies) adds the first companies, mySDR puts a Companies dashboard on the tabs itself, with your own fields as charts, and dashboard_offer says so; after that it offers it as long as it is not there.

Read list_table first to know the fields and stages there are. The reply says what each block shows now: check it matches what the user asked, and give them its dashboard_url. To change it, send the same key again (blocks replaces all blocks); get_dashboard reads what it shows, delete_dashboard removes the tab (the Overview mySDR built does not come back). Free.

Every reply carries a dashboard_url: the Leads, Companies, Signals and Activity views open from those links (they are no tabs anymore). The agenda links the Dashboard (/en/gtm/actions), with due_url for the table on what is due. The links /en/gtm/companies?due=today and ?status=needs_decision (on Companies and Leads) open the table on what is due and on what waits for the user; /en/gtm/activity opens the whole log; the Inbox is the first tab of the Dashboard.