Skip to content
Preview: outbound-sdk is not on npm yet and its repository is private. Instantly and HeyReach are beta, Smartlead is unverified.What is verified
outbound-sdk
Esc
↑↓navigate↵open⌘Jpreview
On this page

exporter

Stream every record of a listing to a CSV or JSON Lines file as pages arrive, or to a writer of your own for Parquet, DuckDB or anything else.

exporter() from outbound-sdk/exporter adds outbound.export(source, { to }). It writes every record of a listing to a .csv or .jsonl file, one row at a time as pages arrive, so a large export never sits in memory. For another format, pass a RowWriter with two functions. The package adds no dependency for Parquet or DuckDB. Writing to a path uses node:fs.

Sample

Runs on the shared seed.

import { mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";

import { createOutbound } from "outbound-sdk";
import { exporter } from "outbound-sdk/exporter";
import { memory } from "outbound-sdk/memory";

import { seed } from "../plugins-index/seed.ts";

const outbound = createOutbound({
  adapter: memory({ data: seed, workspaceId: "agency" }),
  plugins: [exporter()],
});

const dir = await mkdtemp(path.join(tmpdir(), "outbound-export-"));
const csv = path.join(dir, "leads.csv");

// Any listing works as the source. Pages are fetched as rows are written.
const result = await outbound.export(outbound.leads.listAll(), {
  columns: ["ref", "email", "firstName", "company", "custom"],
  to: csv,
});

console.log(result);

console.log(await readFile(csv, "utf-8"));

// JSON Lines, with the default columns: every field of the first record
// except `raw`.
const jsonl = path.join(dir, "campaigns.jsonl");

const campaigns = await outbound.export(outbound.campaigns.listAll(), {
  to: jsonl,
});

console.log(campaigns.columns.join(", "));

const lines = await readFile(jsonl, "utf-8");

console.log(lines.split("\n")[0]);

await rm(dir, { force: true, recursive: true });
{
  columns: [ 'ref', 'email', 'firstName', 'company', 'custom' ],
  rows: 4
}
ref,email,firstName,company,custom
memory:agency:lead_jane,jane.doe@example.com,Jane,Northwind,"{""phone"":""555-0100""}"
memory:agency:lead_sam,sam.lee@example.com,Sam,Contoso,{}
memory:agency:lead_priya,priya.patel@example.com,Priya,Fabrikam,{}
memory:agency:lead_omar,omar.haddad@example.com,Omar,Tailspin,{}

provider, ref, schemaVersion, sourceId, workspaceId, channel, createdAt, name, status, tags
{"provider":"memory","ref":"memory:agency:cmp_founders","schemaVersion":1,"sourceId":"cmp_founders","workspaceId":"agency","channel":"email","createdAt":null,"name":"Acme: SaaS founders","status":"active","tags":["acme"]}

Options

exporter() takes no options.

outbound.export(source, options):

Option Type Default Meaning
source AsyncIterable required Anything that yields records: outbound.leads.listAll(), a campaign’s leads.listAll(), or a generator of your own.
to string | RowWriter required A file path ending in .csv or .jsonl, or a writer. An existing file is overwritten.
columns string[] the first record’s fields, without raw The columns to write, in order.
includeRaw boolean false Keep raw, the vendor’s payload, among the default columns. It has no effect when columns is set.

It returns { rows, columns }.

How rows are written

Format Row Header
.csv The listed columns, comma separated. null and a missing field are empty. An object or array is written as JSON. A cell holding a comma, a double quote or a line break is quoted, with quotes doubled. Lines end with \n. The column names. For an empty listing, the columns you named. When you named none there is no header and the file is empty.
.jsonl One JSON object per line with exactly the listed columns. A missing field is null. None
RowWriter A JSON object with exactly the listed columns, in order, as for .jsonl. columns and includeRaw apply to it as they do to a file. A missing field is null. Up to you

The default columns come from the first record only. A later record with a field the first one lacked does not get a column. Name columns when records vary.

An item of the source that is not an object is skipped and not counted.

A writer of your own

A RowWriter is write(row) and close(), both async. export awaits each write before reading the next record, so a slow writer slows the listing down and does not build a queue.

import { createOutbound, OutboundError } from "outbound-sdk";
import type { Lead } from "outbound-sdk";
import { exporter } from "outbound-sdk/exporter";
import type { RowWriter } from "outbound-sdk/exporter";
import { memory } from "outbound-sdk/memory";

import { seed } from "../plugins-index/seed.ts";

const outbound = createOutbound({
  adapter: memory({ data: seed, workspaceId: "agency" }),
  plugins: [exporter()],
});

// A writer is two async functions. This one collects rows in batches of
// three, the way an insert into DuckDB or a Parquet row group would.
const batchWriter = (label: string): RowWriter => {
  const batches: number[] = [];
  let batch: string[] = [];

  return {
    close: () => {
      if (batch.length > 0) {
        batches.push(batch.length);
      }

      console.log(`${label}: closed after batches of`, batches);

      return Promise.resolve();
    },
    write: (row) => {
      batch.push(String(row.ref));

      if (batch.length === 3) {
        batches.push(batch.length);
        batch = [];
      }

      return Promise.resolve();
    },
  };
};

const result = await outbound.export(outbound.leads.listAll(), {
  to: batchWriter("all leads"),
});

console.log("rows:", result.rows);

// A path that is neither .csv nor .jsonl is refused.
try {
  await outbound.export(outbound.leads.listAll(), { to: "leads.parquet" });
} catch (error) {
  if (OutboundError.is(error)) {
    console.log(`${error.code}: ${error.message}`);
  }
}

// A source that fails part way: the error reaches you, and the writer is
// closed with the rows it already has.
const interrupted = async function* interrupted(): AsyncGenerator<Lead> {
  yield* outbound.leads.listAll({ max: 2 });

  throw new Error("connection lost");
};

try {
  await outbound.export(interrupted(), { to: batchWriter("interrupted") });
} catch (error) {
  console.log("failed:", error instanceof Error ? error.message : error);
}
all leads: closed after batches of [ 3, 1 ]
rows: 4
Invalid: export() writes .csv or .jsonl files; pass a writer for other formats
interrupted: closed after batches of [ 2 ]
failed: connection lost

What it changes

It changes no capability, no record and nothing in meta. It has no layer of its own, so its position in plugins does not matter.

With other plugins

export writes whatever the source yields. A listing from the instance has been through every layer:

  • With redact, the file holds masked values and raw is null.
  • Without it, the file holds the leads’ addresses and names. It is your file, on your disk. The SDK keeps no copy.
  • With clients or identity, the added fields are columns when the first record has them.
  • A budget or maxRequests cap stops the listing part way, as below.

When it fails

What happened What you get What to do
The path ends in neither .csv nor .jsonl Invalid, when the first record arrives or an empty listing ends. Nothing is written. Use one of the two, or pass a writer.
The source fails part way The source’s error. The file is closed and keeps the rows written so far. Do not treat the file as complete. Export again, or resume from the listing’s cursor into a new file.
The source fails before its first record The source’s error. No file is created. A writer you passed is closed without a row. Nothing. close is called once whatever happens.
write or close rejects That error Nothing more is written.
The listing is empty rows: 0. A CSV holds only its header line, or nothing when you named no columns. A .jsonl file is empty, and a writer is closed without a row. Nothing.

Limits

  • CSV cells are not escaped against spreadsheet formulas. A value that starts with =, +, - or @ is written as it is. Lead fields come from outside your company, so treat an exported CSV as untrusted when opening it in a spreadsheet.
  • There is no append mode and no built-in resume. For a copy you keep in step run after run, use sync.

Was this page helpful?