See everyone you have contacted at one company before a call, from its domain
One function that takes a company domain and returns each person at it across campaigns and tools, with who replied, who the vendor marked interested and who replied last. People are matched by exact email domain only.
Ten minutes before a call with someone at a company, you want to know who else there you have written to, and what came back. accountSummary takes the company’s domain, or any email address at it, and returns each person whose address is at that domain: name, title, the campaigns they are in, their status and interest label in each, and open, click and reply counts. It adds totals and the most recent reply.
It matches on the exact email domain and nothing else. A lead the tool knows only by a LinkedIn profile is not found, even when its company field names the company. The answer says how many leads it read that had no address.
Before you start
- Node 22 or later.
- The code below runs on the in-memory adapter, so it needs no vendor key. Each sample on this page is a file in the repository that the test suite runs; the output shown is what it printed.
- For real data: an API key for each tool. A read-only key is enough. No method of the SDK writes to a vendor.
What you get
northwind.example: 3 people in 3 campaigns, 2 replied, 1 marked interested
Last reply: Ava Chen on 2026-11-12
Ava Chen <ava.chen@northwind.example>, CEO
replied, interested; replies 2, clicks 1, opens 7
in mailtool:agency:em_founders: replied, Interested
in mailtool:agency:em_ops: active
in lntool:agency:li_execs: active
Ben Ortiz <ben.ortiz@northwind.example>, Head of Operations
replied, labelled, not as interested; replies 1, clicks 0, opens 3
in mailtool:agency:em_ops: replied, Not Interested
Cy Dunn <cy.dunn@northwind.example>
no reply, no label; replies 0, clicks 0, opens 1
in mailtool:agency:em_founders: active
Read 7 leads. 1 have no email address, so a domain cannot match them.
Write the brief
import type { Outbound } from "outbound-sdk";
import type { InsightsExtension } from "outbound-sdk/insights";
// A count the vendor does not report is null, never 0. Say so.
const count = (value: number | null): string =>
value === null ? "not reported" : String(value);
// `interested` is null when the person carries no label at all.
const interest = (interested: boolean | null): string => {
if (interested === null) {
return "no label";
}
return interested ? "interested" : "labelled, not as interested";
};
/** Everyone the instance holds at one company domain, as text. */
export const accountBrief = async (
outbound: Outbound & InsightsExtension,
domain: string
): Promise<string> => {
const { data, meta } = await outbound.insights.accountSummary({ domain });
const { lastReply, totals } = data;
const lines = [
`${data.domain}: ${totals.people} people in ${totals.campaigns} campaigns, ` +
`${totals.replied} replied, ${totals.interested} marked interested`,
lastReply === null
? "No reply with a known time."
: `Last reply: ${lastReply.name ?? lastReply.email} on ${lastReply.at.slice(0, 10)}`,
"",
];
// Most recent reply first. That order comes from the insight.
for (const person of data.people) {
lines.push(
` ${person.name ?? "No name"} <${person.email}>${
person.title === null ? "" : `, ${person.title}`
}`,
` ${person.replied ? "replied" : "no reply"}, ${interest(person.interested)}; ` +
`replies ${count(person.replies)}, clicks ${count(person.clicks)}, opens ${count(person.opens)}`,
...person.records.map(
(record) =>
` in ${record.campaign ?? "no campaign"}: ${record.status}${record.interest === null ? "" : `, ${record.interest}`}`
)
);
}
lines.push(
"",
`Read ${totals.leadsRead} leads. ${totals.leadsWithoutEmail} have no email address, so a domain cannot match them.`
);
if (!meta.complete || meta.notes.length > 0) {
lines.push("", ...meta.notes.map((note) => `Note: ${note}`));
}
return lines.join("\n");
};
What the insight does:
- It reads leads: from the tool’s list of every lead when the adapter has one, otherwise campaign by campaign, up to
max_campaignscampaigns. Either way it stops atmax_leads(default 1,000). - It keeps the leads whose address is at the domain, and groups them by address. One person in three campaigns is one entry with three
records. - For each person it adds up the counts over their records, and takes the first known name, title and company.
- It sorts people by most recent reply, then people who replied at an unknown time, then the rest by address.
Three things in the answer are worth copying:
interestedhas three values.truewhen one of the person’s labels is a positive one,falsewhen they carry a label and none is positive,nullwhen they carry no label. “No label” is not “not interested”.- A count is
nullwhen the vendor reports none. A sum over records ignores the unknown ones: Ava’s LinkedIn record has no counts, and her totals come from the two email records. totals.leadsWithoutEmailtells you what the domain could not see. One lead in the output above has no address. That is Eve, who works at the company.
Records name their campaign by ref. To print a campaign’s name, read it with outbound.campaign(ref).get(), which is one request each.
Try it without a key
Two made-up tools and four people at one company, one of them known by profile only.
// Two made-up tools for one agency, with four people at one company. The
// company's domain is northwind.example: `.example` is reserved, so it is
// nobody's real domain.
import type { MemoryData } from "outbound-sdk/memory";
export const DOMAIN = "northwind.example";
export const emailTool: MemoryData = {
campaigns: [
{ id: "em_founders", name: "Founders Q4" },
{ id: "em_ops", name: "Ops leaders" },
],
leads: [
// Ava is in both email campaigns. She replied twice in the first.
{
campaignId: "em_founders",
company: "Northwind",
email: "ava.chen@northwind.example",
engagement: {
clicks: 1,
lastContactedAt: "2026-11-10T09:00:00.000Z",
lastRepliedAt: "2026-11-12T15:00:00.000Z",
opens: 5,
replies: 2,
},
firstName: "Ava",
id: "f_ava",
interest: "Interested",
lastName: "Chen",
status: "replied",
title: "CEO",
},
{
campaignId: "em_ops",
email: "ava.chen@northwind.example",
engagement: { clicks: 0, opens: 2, replies: 0 },
id: "o_ava",
},
// Ben replied once and was labelled not interested.
{
campaignId: "em_ops",
company: "Northwind",
email: "ben.ortiz@northwind.example",
engagement: {
clicks: 0,
lastRepliedAt: "2026-11-05T09:00:00.000Z",
opens: 3,
replies: 1,
},
firstName: "Ben",
id: "o_ben",
interest: "Not Interested",
lastName: "Ortiz",
status: "replied",
title: "Head of Operations",
},
// Cy opened one email. No label.
{
campaignId: "em_founders",
email: "cy.dunn@northwind.example",
engagement: { clicks: 0, opens: 1, replies: 0 },
firstName: "Cy",
id: "f_cy",
lastName: "Dunn",
},
// Someone at another company.
{ campaignId: "em_founders", email: "dee@tailspin.example", id: "f_dee" },
],
};
export const linkedinTool: MemoryData = {
campaigns: [
{ channel: "linkedin", id: "li_execs", name: "LinkedIn executives" },
],
leads: [
// The LinkedIn tool holds Ava's address, so her record here joins the rest.
{
campaignId: "li_execs",
email: "ava.chen@northwind.example",
id: "p_ava",
linkedinUrl: "https://www.linkedin.com/in/ava-chen-example",
},
// Eve works at Northwind, but the tool has her profile only. A domain
// cannot find her.
{
campaignId: "li_execs",
company: "Northwind",
firstName: "Eve",
id: "p_eve",
linkedinUrl: "https://www.linkedin.com/in/eve-marsh-example",
},
],
};
import { createOutbound } from "outbound-sdk";
import { combine } from "outbound-sdk/combine";
import { insights } from "outbound-sdk/insights";
import { memory } from "outbound-sdk/memory";
import { accountBrief } from "./brief.ts";
import { DOMAIN, emailTool, linkedinTool } from "./seed.ts";
// Two tools behind one instance. `name` stands in for the vendor.
const outbound = createOutbound({
adapter: combine([
memory({ data: emailTool, name: "mailtool", workspaceId: "agency" }),
memory({ data: linkedinTool, name: "lntool", workspaceId: "agency" }),
]),
plugins: [insights()],
});
// An email address works too: its domain is used.
console.log(await accountBrief(outbound, DOMAIN));
That is the script that printed the brief above.
Point it at your vendor
Swap the adapters. Nothing else changes. One tool works too.
import { createOutbound } from "outbound-sdk";
import { combine } from "outbound-sdk/combine";
import { heyreach } from "outbound-sdk/heyreach";
import { insights } from "outbound-sdk/insights";
import { instantly } from "outbound-sdk/instantly";
// Reads INSTANTLY_API_KEY and HEYREACH_API_KEY. One tool works too: pass
// its adapter alone.
export const outbound = createOutbound({
adapter: combine([
instantly({ workspaceId: "agency" }),
heyreach({ workspaceId: "agency" }),
]),
// One brief may spend at most 60 vendor requests.
maxRequests: 60,
plugins: [insights()],
});
A combined instance promises only what every tool behind it can do. With HeyReach in it, there is no list of every lead, so leads are read campaign by campaign, and the answer carries a note that names the tools whose records have no per-lead counts. Records from a tool that does report counts still carry them.
What each vendor can serve
The insight, run on the in-memory adapter limited to what each vendor’s adapter declares. This shows which calls a vendor’s adapter would refuse and which notes it adds. It does not show what that vendor’s API returns.
import { createOutbound, OutboundError } from "outbound-sdk";
import { heyreach } from "outbound-sdk/heyreach";
import { insights } from "outbound-sdk/insights";
import { instantly } from "outbound-sdk/instantly";
import { memory } from "outbound-sdk/memory";
import { smartlead } from "outbound-sdk/smartlead";
import { VENDORS } from "../_shared/as-vendor.ts";
import { DOMAIN, emailTool } from "./seed.ts";
// What each vendor's adapter can do once an instance has resolved it: a
// capability the adapter leaves out is `false` here. This guide turns on
// `leads.engagement`, which Smartlead's adapter leaves out.
const DECLARED = {
heyreach: () =>
createOutbound({ adapter: heyreach({ apiKey: "not-used" }) }).capabilities,
instantly: () =>
createOutbound({ adapter: instantly({ apiKey: "not-used" }) }).capabilities,
smartlead: () =>
createOutbound({ adapter: smartlead({ apiKey: "not-used" }) }).capabilities,
};
// Counts are not printed here: the in-memory data has counts a real vendor
// may not report. Only what is refused, and what is noted, carries over.
for (const vendor of VENDORS) {
const outbound = createOutbound({
adapter: memory({
capabilities: DECLARED[vendor](),
data: emailTool,
name: vendor,
workspaceId: "agency",
}),
plugins: [insights()],
});
try {
// oxlint-disable-next-line no-await-in-loop -- one vendor at a time.
const { meta } = await outbound.insights.accountSummary({
domain: DOMAIN,
});
console.log(
`${vendor}: ok, leads read ${outbound.capabilities["leads.list"] ? "from one list" : "campaign by campaign"}`
);
for (const note of meta.notes) {
console.log(` note: ${note}`);
}
} catch (error) {
if (!OutboundError.is(error)) {
throw error;
}
console.log(`${vendor}: ${error.code}: ${error.message}`);
}
}
instantly: ok, leads read from one list
smartlead: ok, leads read from one list
note: This tool reports no per-lead open, click or reply counts, so those are null.
heyreach: ok, leads read campaign by campaign
note: This tool reports no per-lead open, click or reply counts, so those are null.
No adapter refuses the call: it needs leads by list or by campaign, and each has one of them. The Smartlead and HeyReach adapters report no per-lead open, click or reply counts, so those are null for them and the brief shows who is in which campaign, their status and their label. On HeyReach a lead is found only when HeyReach holds an email address for it.
What it costs
One request per page of leads read. max_leads (default 1,000, at most 20,000) sets the ceiling, and a vendor request returns about 50 to 100 leads, so a full read at the default is roughly 10 to 20 requests. It reads every lead to find the few at the domain: there is no lookup by domain. When leads are read campaign by campaign, add one request per page of campaigns; max_campaigns defaults to 20. maxRequests on the instance caps the whole call; past it the call fails with RateLimited instead of spending your vendor quota.
When it cannot answer
import { createOutbound, OutboundError } from "outbound-sdk";
import { insights } from "outbound-sdk/insights";
import { memory } from "outbound-sdk/memory";
import { DOMAIN, emailTool } from "./seed.ts";
const outbound = createOutbound({
adapter: memory({ data: emailTool, workspaceId: "agency" }),
plugins: [insights()],
});
// 1. An email provider's domain is not one company.
try {
await outbound.insights.accountSummary({ domain: "someone@gmail.com" });
} catch (error) {
if (OutboundError.is(error)) {
console.log("free domain:", error.code, "-", error.message);
}
}
// 2. A company name is not a domain. Nothing is matched by name.
try {
await outbound.insights.accountSummary({ domain: "Northwind" });
} catch (error) {
if (OutboundError.is(error)) {
console.log("a name:", error.code, "-", error.message);
}
}
// 3. Nobody at the domain. That is an answer, not an error.
const nobody = await outbound.insights.accountSummary({
domain: "initech.example",
});
console.log(
"nobody:",
nobody.data.totals.people,
"people in",
nobody.data.totals.leadsRead,
"leads read"
);
// 4. max_leads stopped the read: people past the cap are missing.
const capped = await outbound.insights.accountSummary({
domain: DOMAIN,
max_leads: 2,
});
console.log(
"capped:",
capped.data.totals.people,
capped.meta.complete,
capped.meta.notes
);
// 5. An adapter that can list leads neither way refuses the insight.
const noLeads = createOutbound({
adapter: memory({
capabilities: { "leads.byCampaign": false, "leads.list": false },
data: emailTool,
}),
plugins: [insights()],
});
try {
await noLeads.insights.accountSummary({ domain: DOMAIN });
} catch (error) {
if (OutboundError.is(error)) {
console.log("no leads:", error.code, "-", error.message);
}
}
free domain: Invalid - gmail.com is an email provider, not one company. Pass allow_free_domains: true to read it anyway.
a name: Invalid - "Northwind" is not a domain or an email address
nobody: 0 people in 5 leads read
capped: 1 false [ 'Stopped at 2 leads; more exist.' ]
no leads: Unsupported - account_summary is not available: the memory adapter supports neither leads.list nor leads.byCampaign
| What happened | What you get | What to do |
|---|---|---|
The domain is an email provider such as gmail.com |
Invalid: it is not one company |
Pass the company’s own domain, or allow_free_domains: true if you mean it. |
| Text that is not a domain or an address, such as a company name | Invalid, before any request |
Pass acme.com, an address at it, or its URL. |
| Nobody at the domain | people is empty and totals.leadsRead says how many leads were looked at |
Check the spelling, and check meta.complete. |
More leads than max_leads, or more campaigns than max_campaigns |
meta.complete is false and a note says where it stopped. People past the cap are missing. |
Raise the cap. |
| The adapter can list leads neither way | Unsupported, before any request |
Use an adapter that declares leads.list or leads.byCampaign. |
| A rejected key | Unauthorized. It is not retried. |
Check the key and its scope. |
Limits
- Exact domain only.
acme.comdoes not find people ateu.acme.comoracme.co.uk, and a person who signed up with a personal address is not at the company as far as this goes. - LinkedIn-only leads are never found.
totals.leadsWithoutEmailcounts every lead without an address that was read, at any company. - When leads are read campaign by campaign, every campaign is read whatever its status, in the order the vendor lists them, until a cap is reached.
lastReplyis the most recent reply with a known time. A person the vendor marks as replied with no time is counted intotals.repliedand cannot belastReply.- On an
unverifiedadapter, check a brief against the vendor’s own lead view before a call.