Code & Snippets

Pull All CRM Notes for a Company (Attio).

Paginates Attio's notes API for a single company, drops automated pre-meeting briefs, and returns a clean author + content array — drop-in for auto-populating a company's drive folder with full CRM history.

Pure Operations
JavaScript · 97 lines
C.1

Download

Grab the source file. Drop it into your service, set the required environment variables, and deploy.

C.2

How it works

The gist

Pulls every CRM note attached to a single company record in Attio, filters out the automated pre-meeting briefs, and returns a clean { author, content } array — typically used to auto-populate a fresh company drive folder (e.g. an "AI Generated Outputs" folder) with the full CRM history at the moment the folder is scaffolded.

The flow

  1. Inputs. ATTIO_TOKEN, COMPANY_RECORD_ID, optional PARENT_OBJECT (defaults to companies), LIMIT (default 49), and MAX_PAGES (default 50).
  2. Paginate /v2/notes with parent_object + parent_record_id, stopping when a page returns fewer than LIMIT results or MAX_PAGES is hit. 200ms delay between pages to stay polite.
  3. Drop pre-meeting notes (anything whose title normalizes to contain "premeeting") so the export is just human-written context.
  4. Simplify each note to { author, content }. Author is parsed from titles like "Note by Jane Doe"; content prefers plaintext and falls back to markdown.
  5. Return a single payload field holding the stringified JSON array — single field so Zapier doesn't try to expand the result into per-note line items.

Swapping CRMs

This is Attio-specific, but the structure is generic. To run against another CRM (Affinity, Salesforce, HubSpot, etc.) swap:

  • BASE_URL → that CRM's notes endpoint
  • The Authorization header for whatever auth scheme the CRM uses
  • The pagination params (offset/limit vs. cursor) to match the API
  • The field mapping inside simplify() to that CRM's note shape

The pagination loop, pre-meeting filter, and Zapier output shape stay the same.

Why this exists

When a new company gets a drive folder, the most valuable thing you can drop into it is everything the firm has already written about that company. This snippet is the one call that pulls that history out of the CRM in one shot, ready for the next step to write into the folder.

C.3

Source

Full source, exactly as shipped. The download above is byte-identical.

attio_company_notes.jsJavaScript
/**
 * Pull All CRM Notes for a Company (Attio)
 *
 * One-step "Run JavaScript" snippet that calls the Attio API, paginates
 * through every note on a given company record, filters out automated
 * pre-meeting briefs, and returns a simplified array of `{ author, content }`.
 *
 * Typical use: feed this into a downstream step that writes those notes
 * into the company's drive folder so a freshly-spun-up "AI Generated
 * Outputs" folder is auto-populated with the full CRM history.
 *
 * Swappable: this is Attio-specific. To run against a different CRM
 * (Affinity, Salesforce, HubSpot, etc.) swap the BASE_URL, auth header,
 * pagination params, and the `simplify()` field mapping for that CRM's
 * notes endpoint — the rest of the pagination + filtering shape stays
 * identical.
 *
 * Output: `{ payload: <stringified JSON array> }` — single field so Zapier
 * doesn't try to expand the result into line items.
 */

const ATTIO_TOKEN = inputData.ATTIO_TOKEN;
const COMPANY_RECORD_ID = inputData.COMPANY_RECORD_ID;
const PARENT_OBJECT = inputData.PARENT_OBJECT || "companies";
const LIMIT = Number(inputData.LIMIT || 49);
const MAX_PAGES = Number(inputData.MAX_PAGES || 50);

if (!ATTIO_TOKEN) throw new Error("Missing ATTIO_TOKEN");
if (!COMPANY_RECORD_ID) throw new Error("Missing COMPANY_RECORD_ID");

const BASE_URL = "https://api.attio.com/v2/notes";
const delay = (ms) => new Promise((res) => setTimeout(res, ms));

function extractAuthor(title) {
  if (!title) return null;
  const match = title.match(/^Note by\s+(.+)/i);
  return match ? match[1].trim() : title.trim();
}

function isPreMeetingNote(note) {
  const normalized = (note?.title || "").toLowerCase().replace(/[\s-]/g, "");
  return normalized.includes("premeeting");
}

function simplify(note) {
  return {
    author: extractAuthor(note?.title),
    content: note?.content_plaintext || note?.content_markdown || ""
  };
}

async function fetchPage(offset) {
  const url = new URL(BASE_URL);
  url.searchParams.set("parent_object", PARENT_OBJECT);
  url.searchParams.set("parent_record_id", COMPANY_RECORD_ID);
  url.searchParams.set("limit", String(LIMIT));
  url.searchParams.set("offset", String(offset));

  const headers = {
    "Authorization": `Bearer ${ATTIO_TOKEN}`,
    "Accept": "application/json"
  };

  const res = await fetch(url.toString(), { method: "GET", headers });
  if (!res.ok) {
    const text = await res.text();
    throw new Error(`Attio /v2/notes failed ${res.status}: ${text}`);
  }

  return res.json();
}

async function getAllNotes() {
  let all = [];
  let offset = 0;
  let pages = 0;

  while (pages < MAX_PAGES) {
    const data = await fetchPage(offset);
    const notes = Array.isArray(data?.data) ? data.data : [];
    all.push(...notes);
    if (notes.length < LIMIT) break;
    offset += LIMIT;
    pages += 1;
    await delay(200);
  }

  return all.filter((note) => !isPreMeetingNote(note)).map(simplify);
}

const notes = await getAllNotes();

// Single-field return: Zapier won't expand into line items
return {
  payload: JSON.stringify(notes)
};