Skip to main content
Most GTM teams don’t have one source of leads — they have five. Product signups, community Slack, inbound forms, outbound prospecting, and market signals all need to land in the same CRM, enriched and routed, without a human gluing it together. This page shows a three-workflow pipeline that pulls leads from very different sources and funnels them into Attio. Each workflow is independent — deploy one, deploy all three, or fork them.
Some of the integrations, triggers, and SDK features below (WorkOS, Parallel Web Services, Parallel Monitor, structured agent output) are on the roadmap. The code samples reflect what the API will look like. Join the waitlist from Integrations in the app if you want early access.

Start from a project scaffold

Go to your folder of choice and scaffold a new Terse Project.
terse init lead-gen
When prompted, connect WorkOS, Attio and Slack as integrations. If needed, you can re-configure your integrations in CLI or use our web-UI.
terse integrate
Connect WorkOS, Parallel, Slack, Attio, and an outbound email provider from Integrations, then run terse generate to refresh the typed helpers in src/terse.generated.ts.

The three entry points

  1. Product signups — a daily digest of new WorkOS workspaces, enriched and emailed to GTM as a reviewable CSV.
  2. Community Slack — every new member of your community channel lands in Attio, enriched, in real time.
  3. Market signals — Parallel Monitor watches the web for fundraising announcements from ICP-fit companies and opens them as Attio records.

1. Daily signups from WorkOS

Run once a day, pull new workspaces from WorkOS, enrich each one with Parallel, and email a CSV of proposed Attio updates to the GTM team for review. This keeps a human in the loop while still doing all the grunt work.

What it does

  1. Runs every day at 8am.
  2. Deterministically fetches WorkOS workspaces created in the window.
  3. For each workspace, an agent decides create vs update against Attio and enriches via Parallel Web Services.
  4. The workflow collects the draft rows, builds a CSV, and emails it as an attachment.
Keeping the fetch deterministic means the batch is reproducible. Letting the agent decide create vs update keeps the fuzzy parts — matching on domain, filling only missing fields, flagging conflicts — out of hand-written branching.

Core workflow

import { CronTrigger, Terse, TerseAgent } from "terse-sdk"
import { z } from "zod"

import { Attio, AttioList, Email, Parallel, Schedule, WorkOs } from "./terse.generated"

const DraftRow = z.object({
    action: z.enum(["create", "update", "skip"]),
    attio_record_id: z.string().nullable(),
    workspace_id: z.string(),
    company_name: z.string(),
    domain: z.string(),
    signed_up_at: z.string(),
    industry: z.string(),
    employees: z.number(),
    revenue: z.string(),
    icp_fit: z.number(),
    notes: z.string()
})

const client = new Terse()

await client.createJob({
    name: "workos-daily-signup-digest",
    triggers: [Schedule.cron({ expression: "0 8 * * *" })],
    skills: [WorkOs.skill(), Parallel.skill(), Attio.skill({ lists: [AttioList.Accounts.Signups] }), Email.skill()],
    onTrigger: async (event: CronTrigger, agent: TerseAgent) => {
        const workspaces = await agent.tools.workos.listWorkspaces({
            createdAfter: event.window.startsAt,
            createdBefore: event.window.endsAt
        })

        if (workspaces.length === 0) return

        const rows = await Promise.all(
            workspaces.map(w =>
                agent.runAndWait(
                    {
                        instructions: [
                            `Search ${AttioList.Accounts.Signups} for an existing record matching domain "${w.domain}".`,
                            "If a record exists, propose 'update' and fill only the fields that are missing or stale.",
                            "If no record exists, propose 'create' with all enriched fields.",
                            "If the domain is clearly not a business (personal email, test data), propose 'skip' and leave enrichment empty.",
                            "Enrich via Parallel Web Services: industry, employee count, estimated revenue, ICP fit score.",
                            "Use the notes field to flag conflicts, low-confidence enrichment, or anything a human should review.",
                            `Workspace: ${JSON.stringify(w)}`
                        ].join("\n"),
                        output: DraftRow
                    },
                    event
                )
            )
        )

        const csv = agent.utils.toCsv(rows)

        await agent.tools.email.send({
            to: "gtm@acme.com",
            subject: `New signups — ${event.window.label} (${rows.length})`,
            body: "Attached: draft Attio actions for today's WorkOS signups. Each row is tagged create, update, or skip. Reply with edits or approve to sync.",
            attachments: [{ filename: "attio-drafts.csv", contentType: "text/csv", content: csv }]
        })
    }
})
The agent loop dedupes on a best-effort basis. Two workflows writing to the same Attio list at the same time (for example this digest and the community Slack workflow below) can still race. Add a uniqueness constraint on domain in Attio, or run a follow-up reconciliation workflow, if you need hard guarantees.

Customize

  • Swap WorkOs.skill() for Clerk, Auth0, or Snowflake when those integrations ship — the rest of the workflow stays the same.
  • Drop the email step and let the agent write directly to Attio once the GTM team trusts the enrichment.
  • Tighten the instructions to icp_fit >= 70 if you only want high-fit signups in the daily digest.

2. New Slack community members

Every new member in your community Slack channel is a warm lead. Enrich them the moment they join and drop them into Attio so AEs can follow up without anyone opening Slack Audit.

What it does

  1. Triggers when a member joins your community channel.
  2. Reads their Slack profile for name, email, and title.
  3. Enriches their company with Parallel.
  4. Upserts a contact in Attio with the enriched record.

Core workflow

import { Terse, TerseAgent, Trigger } from "terse-sdk"

import { Attio, AttioList, Parallel, Slack, SlackChannel } from "./terse.generated"

const client = new Terse()

await client.createJob({
    name: "community-slack-to-attio",
    triggers: [Slack.onMemberJoinedChannel({ channel: SlackChannel.Community })],
    skills: [Slack.skill({ channel: SlackChannel.Community }), Parallel.skill(), Attio.skill({ lists: [AttioList.Contacts.Community] })],
    onTrigger: async (event: Trigger, agent: TerseAgent) => {
        const profile = await agent.tools.slack.getUserProfile({ userId: event.userId })

        const domain = profile.email?.split("@")[1]
        if (!domain) return

        const research = await agent.tools.parallel.research({
            domain,
            fields: ["companyName", "industry", "employeeCount", "icpFitScore"]
        })

        await agent.tools.attio.upsertRecord({
            list: AttioList.Contacts.Community,
            matchOn: "email",
            fields: {
                email: profile.email,
                full_name: profile.realName,
                title: profile.title,
                company_name: research.companyName,
                company_domain: domain,
                industry: research.industry,
                employees: research.employeeCount,
                icp_fit: research.icpFitScore,
                source: "community-slack"
            }
        })
    }
})

Customize

  • Add a Slack DM welcoming the new member and linking to your onboarding doc.
  • Route high-ICP contacts to a specific AE by tagging the Attio record with an owner.
  • Skip enrichment for personal email domains if you don’t want them in the pipeline.

3. Fundraising signals from Parallel Monitor

Parallel Monitor watches the open web for signals you care about. Wire it to fundraising announcements that match your ICP and let the workflow open the Attio record before a human ever reads the news.

What it does

  1. Triggers on a Parallel Monitor fundraising signal.
  2. Filters to companies that match ICP (industry, region, headcount).
  3. Uses the model to draft a one-paragraph account brief.
  4. Creates an Attio record in the outbound list with the brief and signal context.

Core workflow

import { Terse, TerseAgent, Trigger } from "terse-sdk"

import { Attio, AttioList, Parallel } from "./terse.generated"

const client = new Terse()

await client.createJob({
    name: "fundraising-signals-to-attio",
    triggers: [
        Parallel.Monitor.onSignal({
            topic: Parallel.Signals.FundingAnnouncement,
            match: {
                industries: ["software", "fintech", "healthtech"],
                regions: ["north-america", "europe"],
                minRoundSize: 10_000_000
            }
        })
    ],
    skills: [Parallel.skill(), Attio.skill({ lists: [AttioList.Accounts.Outbound] })],
    onTrigger: async (event: Trigger, agent: TerseAgent) => {
        const research = await agent.tools.parallel.research({
            domain: event.signal.company.domain,
            fields: ["employeeCount", "technologies", "icpFitScore"]
        })

        if (research.icpFitScore < 60) return

        const prompt = [
            "Write a two-sentence outbound brief for an AE.",
            `Company: ${event.signal.company.name}`,
            `Round: ${event.signal.round.stage}${event.signal.round.amountUsd} USD`,
            `Announced: ${event.signal.publishedAt}`,
            `Headline: ${event.signal.headline}`,
            `Source: ${event.signal.url}`,
            `Employees: ${research.employeeCount}`,
            `Stack: ${research.technologies.join(", ")}`
        ].join("\n")

        const brief = await agent.runAndWait(prompt, event)

        await agent.tools.attio.createRecord({
            list: AttioList.Accounts.Outbound,
            fields: {
                company_name: event.signal.company.name,
                company_domain: event.signal.company.domain,
                source: "fundraising-signal",
                signal_url: event.signal.url,
                round_stage: event.signal.round.stage,
                round_amount_usd: event.signal.round.amountUsd,
                icp_fit: research.icpFitScore,
                account_brief: brief
            }
        })
    }
})

Customize

  • Swap the signal topic to hiring spikes, leadership changes, or product launches.
  • Post a Slack alert to the outbound channel in addition to the Attio record.
  • Add a second filter against existing Attio records to skip accounts you’re already working.

Deploy

terse deploy
Each workflow runs independently. Check Activity to watch triggers fire, and Stats to see how many leads each entry point contributes to the pipeline.