Data Components

Data Delivery

This component helps you format the data you receive from Exa as a CSV, JSON, a callable agent tool, a webhook payload, a Slack Block Kit message, or a Salesforce or HubSpot bulk import.

Demo

Source data · 5 enriched prospectsFrom List Building + Data Enrichment
MercuryFintech850$500M94
RampFintech1,100$700M91
BrexFintech950$420M88
RipplingHR Tech3,200$870M82
DeelHR Tech4,500$1B79

Spreadsheet-ready rows. Drop into Clay, Apollo, or Excel.

prospects.csv
Company,Domain,Industry,Headcount,Est ARR,HQ,One-liner,ICP Score
Mercury,mercury.com,Fintech,850,$500M,"San Francisco, CA",Banking stack for startups and scale-ups.,94
Ramp,ramp.com,Fintech,1100,$700M,"New York, NY",Corporate cards and spend management.,91
Brex,brex.com,Fintech,950,$420M,"San Francisco, CA",Corporate cards and treasury for scaling companies.,88
Rippling,rippling.com,HR Tech,3200,$870M,"San Francisco, CA","HR, IT, and finance on one employee record.",82
Deel,deel.com,HR Tech,4500,$1B,"San Francisco, CA",Global payroll and contractor hiring.,79

How it works

One typed Prospect[] array fans out into six destination shapes through plain functions in data-delivery-formats.ts. Use it after List Building and Data Enrichment to hand the result to the next system.

The Agent tool call flavors track the live specs. MCP uses inputSchema plus an optional outputSchema. OpenAI nests the JSON Schema under parameters, tags the wrapper with type: "function", and turns on strict: true with additionalProperties: false on every object. Anthropic uses input_schema (snake_case) and skips the outer type wrapper.

The Webhook payload uses a stable envelope (event, id, created, source, data) so one listener can fan in events from multiple Monitors. The Slack message ships as Block Kit JSON, ready to POST to chat.postMessage.

The CRM tab maps to native field names. Salesforce expects Company, Website, NumberOfEmployees, and custom __c fields for ICP score and revenue. HubSpot uses lowercase property names and wraps records in an inputs array for the batch-create endpoint.

Source code

"use client";

import { useMemo, useState } from "react";
import { Check, Copy, Download } from "lucide-react";
import {
  SAMPLE_PROSPECTS,
  toAnthropicToolSchema,
  toCsv,
  toHubspotCompanies,
  toJson,
  toMcpToolSchema,
  toOpenAIToolSchema,
  toSalesforceLeads,
  toSlackMessage,
  toWebhookPayload,
} from "@/lib/data-delivery-formats";

type DestId = "csv" | "json" | "tool" | "webhook" | "slack" | "crm";
type ToolFlavor = "mcp" | "openai" | "anthropic";
type CrmFlavor = "salesforce" | "hubspot";

const DESTINATIONS: Array<{ id: DestId; label: string; tagline: string }> = [
  {
    id: "csv",
    label: "CSV",
    tagline: "Spreadsheet-ready rows. Drop into Clay, Apollo, or Excel.",
  },
  {
    id: "json",
    label: "JSON",
    tagline: "Raw records for any downstream system or script.",
  },
  {
    id: "tool",
    label: "Agent tool call",
    tagline:
      "Expose this dataset as a callable tool. Same data, three flavors agents already speak.",
  },
  {
    id: "webhook",
    label: "Webhook",
    tagline:
      "Push event to any URL. Fire when a Monitor finds new ICP-fit prospects.",
  },
  {
    id: "slack",
    label: "Slack",
    tagline: "Block Kit message ready for chat.postMessage.",
  },
  {
    id: "crm",
    label: "CRM",
    tagline: "Bulk-create records in Salesforce or HubSpot.",
  },
];

export function DataDeliveryExample() {
  const [dest, setDest] = useState<DestId>("csv");
  const [tool, setTool] = useState<ToolFlavor>("mcp");
  const [crm, setCrm] = useState<CrmFlavor>("salesforce");

  const rows = SAMPLE_PROSPECTS;

  const { code, language, filename } = useMemo(() => {
    switch (dest) {
      case "csv":
        return { code: toCsv(rows), language: "csv", filename: "prospects.csv" };
      case "json":
        return { code: toJson(rows), language: "json", filename: "prospects.json" };
      case "tool": {
        const code =
          tool === "mcp"
            ? toMcpToolSchema()
            : tool === "openai"
              ? toOpenAIToolSchema()
              : toAnthropicToolSchema();
        return { code, language: "json", filename: `${tool}-tool.json` };
      }
      case "webhook":
        return {
          code: toWebhookPayload(rows),
          language: "json",
          filename: "webhook.json",
        };
      case "slack":
        return {
          code: toSlackMessage(rows),
          language: "json",
          filename: "slack-message.json",
        };
      case "crm": {
        const code =
          crm === "salesforce" ? toSalesforceLeads(rows) : toHubspotCompanies(rows);
        return { code, language: "json", filename: `${crm}-import.json` };
      }
    }
  }, [dest, tool, crm, rows]);

  const activeDest = DESTINATIONS.find((d) => d.id === dest)!;

  return (
    <div className="space-y-4">
      <SourcePreview />

      <div className="flex items-center gap-1 border-b border-[#e5e5e5] overflow-x-auto">
        {DESTINATIONS.map((d) => {
          const isActive = d.id === dest;
          return (
            <button
              key={d.id}
              type="button"
              onClick={() => setDest(d.id)}
              className={`px-3 py-2 text-[13px] -mb-px border-b-2 whitespace-nowrap ${
                isActive
                  ? "border-[var(--color-brand)] text-[#111827] font-medium"
                  : "border-transparent text-[#6b7280] hover:text-[#111827]"
              }`}
            >
              {d.label}
            </button>
          );
        })}
      </div>

      <p className="text-[13px] text-[#6b7280]">{activeDest.tagline}</p>

      {dest === "tool" && (
        <SubSelector
          options={[
            { id: "mcp", label: "MCP" },
            { id: "openai", label: "OpenAI" },
            { id: "anthropic", label: "Anthropic" },
          ]}
          value={tool}
          onChange={(v) => setTool(v as ToolFlavor)}
        />
      )}

      {dest === "crm" && (
        <SubSelector
          options={[
            { id: "salesforce", label: "Salesforce" },
            { id: "hubspot", label: "HubSpot" },
          ]}
          value={crm}
          onChange={(v) => setCrm(v as CrmFlavor)}
        />
      )}

      <OutputBlock code={code} language={language} filename={filename} />
    </div>
  );
}

function SubSelector({
  options,
  value,
  onChange,
}: {
  options: Array<{ id: string; label: string }>;
  value: string;
  onChange: (v: string) => void;
}) {
  return (
    <div className="inline-flex border border-[#e5e5e5] bg-white">
      {options.map((o) => {
        const isActive = o.id === value;
        return (
          <button
            key={o.id}
            type="button"
            onClick={() => onChange(o.id)}
            className={`px-3 py-1.5 text-[12px] border-r border-[#e5e5e5] last:border-r-0 ${
              isActive
                ? "bg-[var(--color-brand-faint)] text-[var(--color-brand)] font-medium"
                : "text-[#6b7280] hover:text-[#111827]"
            }`}
          >
            {o.label}
          </button>
        );
      })}
    </div>
  );
}

function SourcePreview() {
  return (
    <div className="border border-[#e5e5e5] bg-white">
      <div className="px-3 py-1.5 border-b border-[#e5e5e5] bg-[#fafafa] flex items-center justify-between">
        <span className="text-[10px] uppercase tracking-wider text-[#9ca3af]">
          Source data · 5 enriched prospects
        </span>
        <span className="text-[10px] text-[#9ca3af]">
          From List Building + Data Enrichment
        </span>
      </div>
      <div className="divide-y divide-[#e5e5e5]">
        {SAMPLE_PROSPECTS.map((p) => (
          <div
            key={p.company}
            className="flex items-center gap-3 px-3 py-1.5 text-[12px]"
          >
            {/* eslint-disable-next-line @next/next/no-img-element */}
            <img
              src={`https://www.google.com/s2/favicons?domain=${p.domain}&sz=64`}
              alt=""
              className="h-3.5 w-3.5 rounded-sm shrink-0"
              loading="lazy"
            />
            <span className="text-[#111827] w-20 truncate">{p.company}</span>
            <span className="text-[#6b7280] w-20 truncate">{p.industry}</span>
            <span className="text-[#6b7280] w-16 truncate tabular-nums">
              {p.headcount.toLocaleString()}
            </span>
            <span className="text-[#6b7280] w-16 truncate tabular-nums">
              {p.estArr}
            </span>
            <span className="text-[#6b7280] flex-1 truncate hidden sm:inline">
              {p.oneLiner}
            </span>
            <span className="inline-flex h-4 items-center rounded-full px-1.5 text-[10px] font-medium bg-[var(--color-brand-faint)] text-[var(--color-brand)] tabular-nums shrink-0">
              {p.icpScore}
            </span>
          </div>
        ))}
      </div>
    </div>
  );
}

function OutputBlock({
  code,
  language,
  filename,
}: {
  code: string;
  language: string;
  filename: string;
}) {
  const [copied, setCopied] = useState(false);

  const handleCopy = async () => {
    try {
      await navigator.clipboard.writeText(code);
      setCopied(true);
      setTimeout(() => setCopied(false), 1500);
    } catch {
      // ignore
    }
  };

  const handleDownload = () => {
    const mime = language === "csv" ? "text/csv" : "application/json";
    const blob = new Blob([code], { type: `${mime};charset=utf-8` });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = filename;
    document.body.appendChild(a);
    a.click();
    a.remove();
    URL.revokeObjectURL(url);
  };

  return (
    <div className="relative group border border-[#e5e5e5] bg-[#fafafa]">
      <div className="flex items-center justify-between border-b border-[#e5e5e5] bg-[#f5f5f5] px-3 py-1.5">
        <span className="text-[11px] font-mono text-[#6b7280]">{filename}</span>
        <div className="flex items-center gap-1">
          <button
            type="button"
            onClick={handleDownload}
            aria-label="Download"
            className="p-1 rounded text-[#6b7280] hover:bg-black/[0.05]"
          >
            <Download size={13} />
          </button>
          <button
            type="button"
            onClick={handleCopy}
            aria-label="Copy"
            className="p-1 rounded text-[#6b7280] hover:bg-black/[0.05]"
          >
            {copied ? <Check size={13} /> : <Copy size={13} />}
          </button>
        </div>
      </div>
      <pre className="p-4 overflow-x-auto text-[12.5px] leading-[1.6] font-mono text-[#111827] max-h-[480px]">
        <code>{code}</code>
      </pre>
    </div>
  );
}