# 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`](/docs/api-reference/save-table-leads)) for people and `save_companies` ([`POST /tables/companies`](/docs/api-reference/save-table-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`](/docs/api-reference/check-before-sending)): 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`](/docs/api-reference/log-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`](/docs/api-reference/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`](/docs/api-reference/set-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`](/docs/api-reference/create-campaign)) 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`](/docs/api-reference/add-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](/docs/signals/keep-listening#webhooks)): outreach and the tables are read this way.

## Read it back

`list_table` ([`GET /tables/leads`](/docs/api-reference/list-table-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}`](/docs/api-reference/get-table-lead)) 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`](/docs/api-reference/save-dashboard)): 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.
