Skip to content

Post-engager harvest

Posts about your problem in, the people who commented on them out, filtered to your buyer’s titles, each with the post they engaged on.

Kind
Workflow
Price
56¢ per 200 people
Your time
7 minutes
Approval
Runs on its own
Deepens
Outbound
Audience.harvest
You, to your agent

Find the property managers who engaged with posts about rent spreadsheets this month, spend up to $2, and show me the plan first.

curl -X POST "https://api.sb/leaseline.app/audiences/harvest?mode=try" \
  -H "Authorization: Bearer $SB_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"goal":"Find the property managers who engaged with posts about rent spreadsheets this month, spend up to $2, and show me the plan first.","budget":{"amount":2,"currency":"USD"}}'
npx startup-builder do Audience.harvest --try --data '{"goal":"Find the property managers who engaged with posts about rent spreadsheets this month, spend up to $2, and show me the plan first.","budget":{"amount":2,"currency":"USD"}}'
{
  "name": "do",
  "arguments": {
    "mode": "try",
    "code": "return await $.Audience.harvest({ goal: 'Find the property managers who engaged with posts about rent spreadsheets this month, spend up to $2, and show me the plan first.', budget: { amount: 2, currency: 'USD' } })"
  }
}
import { $ } from 'startup-builder'

const result = await $.try.Audience.harvest({ goal: 'Find the property managers who engaged with posts about rent spreadsheets this month, spend up to $2, and show me the plan first.', budget: { amount: 2, currency: 'USD' } })
Response
{
  "mode": "try",
  "stored": false,
  "estimate": "About 56¢ per 200 people",
  "sample": {
    "person": "Maya C.",
    "title": "Operations Lead",
    "company": "Cedar Hollow Rentals",
    "engaged on": "a post on rent spreadsheets",
    "how": "comment"
  }
}
Try mode reads, stores nothing and spends nothingAPI referenceIts Nouns

What you get

  • 200 people · posts about your problem in, the people who commented on them out, filtered to your buyer’s titles, each with the post they engaged on. · Audience
  • audience · A warm Audience, ready for Personal first touch or a LinkedIn message.
  • people[] · Name, title, company, the post they engaged on and how: comment or like.
  • bySource · Kept people per post: the posts that draw your buyer, for your own next post.

Connect once: LinkedIn. How long: About 5 minutes.

Trigger

It runs weekly, and two days after each of your own posts goes up.

TypeScript
on.Post.published((post, $) => post.handle.startsWith('linkedin:') && $.Audience.harvest({ posts: { urls: [post.url] }, take: 50 }))

Steps

Your agent does these steps, one row per step; each step’s check is part of its row and its price.

StepVerbReturnsPriceApproval
1. Find posts that use your problem phrasesKeyword.searchRecent LinkedIn posts on the problem, by commentsAbout 1¢Runs on its own
2. Read who engaged with each postProspect.findThe people who commented on or liked each postAbout 15¢Runs on its own
3. Keep your buyer’s titles and company sizesCollection.queryThe people who pass the declared filterFreeRuns on its own
4. Read each kept person’s profileContact.enrichTitle, employer, seniority and location, currentAbout 40¢Runs on its own
5. Save them as a warm AudienceAudience.createOne Audience, each row with the post it came fromFreeRuns on its own

Calls: Keyword.search, then Prospect.find, then Collection.query, then Contact.enrich, then Audience.create.

About 56¢ per 200 people, each time it runs, capped at $1.50.

Example: give it a goal

Find the property managers who engaged with posts about rent spreadsheets this month, spend up to $2, and show me the plan first.

The plan it shows runs 5 steps, priced before it spends anything; Run it above shows the result it returns.

When something fails

Each failure, and what happens next
WhenThen
A post hides its engagementIt is skipped with engagement-hidden; the read costs nothing.
The LinkedIn connection needs signing in againOnly the own-posts part stops, with the fix line; the phrase search still runs.
New posts add under 5% new peopleIn goal mode the agent stops reading posts, as a Decision: more posts would buy the same people.
You call it twice in one day for one postThe second call returns the stored people at $0: it is idempotent per post and day.

Each step retries three times, then falls through to the next route; one item's failure lands in skipped with its reason, never failing the call. Before a step would cross maxCost it stops and returns what it has, with a resume link. Each step's idempotency key is the call's key plus the step and the item: leaseline-audience-harvest-1-<step>-<item>.

Change it

AskCompiles to
Count likes too, not only comments.Audience.harvest
Write each of them a first message that mentions their comment.Prospect.reach
Harvest my own posts every week.Audience.harvest
TypeScript
import { $, workflow } from 'startup-builder'

export default workflow({
  name: 'audience-harvest',
  verb: 'Audience.harvest',
  input: { posts: '{ phrases?: string[]; own?: boolean; urls?: string[] }', how: "('comment' | 'like')[]", take: 'integer' },
  maxCost: 1.5,
  execute: async (ctx, { posts = { own: true }, how = ['comment'], icp = 'icps/icp_4', take = 200 }) => {
    const found = posts.phrases ? await ctx.step('posts', () => $.Keyword.search({ keywords: posts.phrases, venue: 'linkedin', sort: 'comments', maxCost: 0.02 })) : { results: [] }
    const urls = [...found.results.map((r) => r.url), ...(posts.urls ?? [])]
    const who = await ctx.step('engagers', () => $.Prospect.find({ engagedWith: urls, own: posts.own, how, maxCost: 0.2 }))
    const kept = await ctx.step('filter', () => $.Collection.query({ rows: who.prospects, matches: icp, limit: take }))   // a declared filter
    const people = await ctx.step('profiles', () => $.Contact.enrich({ contacts: kept.rows.map((p) => p.$id), fields: ['title', 'organization'], maxCost: 0.5 }))
    return ctx.step('save', () => $.Audience.create({ name: 'Warm · engaged', members: people }))
  },
})

Extend it with extends: 'Audience.harvest' and base(), or compose your own from these steps.

Works with

Plays that order it: Creator partnership, Test with a creator, Creator retainers, Engagement bridge, Events and webinars, Hosted gatherings, Tools on request.

It serves Find your first customers, and runs in Hands-on. It deepens Outbound.

For agents

For agents: the guide, its approvals, retries and lookups
FactValue
GuideAudience.harvest
Guide approvallimits
GoalFind the property managers who engaged with posts about rent spreadsheets this month, spend up to $2, and show me the plan first. It may change counts, the order of steps, soft filters and the route a read takes, and it stops early when the goal is met. It never relaxes your words, raises its own budget, answers its own approval, calls a Play or acts outside your standing approval.
Triggeron.Post.published((post, $) => post.handle.startsWith('linkedin:') && $.Audience.harvest({ posts: { urls: [post.url] }, take: 50 }))
RetryThree times with backoff, then the next route; the same idempotency key never charges, sends or posts twice
Idempotency keyleaseline-audience-harvest-1
LookupsA find: input asks unless exactly one record fits
Step
1. Find posts that use your problem phrases
2. Read who engaged with each post
3. Keep your buyer’s titles and company sizes
4. Read each kept person’s profile
5. Save them as a warm Audience

The amounts live once, on the hub (approvals).

Who does each step, and how it is checked
StepFunctionDoerCheckerAssuranceInputs
1. Find posts that use your problem phrasescodeCodeCode checks each result against its declared typemeasured$0.posts.phrases
2. Read who engaged with each postcodeCodeCode checks each result against its declared typemeasured$1.posts, $0.posts.urls
3. Keep your buyer’s titles and company sizescodeCodeCode checks each result against its declared typemeasured$2.people, $0.icp
4. Read each kept person’s profilecodeCodeCode checks each result against its declared typemeasured$3.people
5. Save them as a warm AudiencecodeCodeCode reads the result back from where it landedmeasured$4, $0.take

The doer is never the checker, and the weakest assurance on the path is the whole call’s.

Under the hood

Under the hood: the Systems, Nouns and Events it touches
KindWhat it touches
SystemsAds, CRM
NounsKeyword, Prospect, Collection, Contact, Audience
EventsCollection.queried, Contact.enriched, Audience.created

Reference

Inputs, outputs, its Event and its errors
NameTypeDefaultMeaning
posts{ phrases?: string[]; own?: boolean; urls?: Url[] }{ own: true }Where to harvest: LinkedIn posts that use your problem phrases, your own posts, or posts you name.
how('comment' | 'like')[]['comment']Which engagement counts. Commenters wrote something you can answer; a like is a weaker sign.
icpICPyour saved ICPThe titles and company sizes kept, as a declared filter.
takeinteger200People to keep.
maxCostnumber1.50The most this call spends each time it runs, in USD. It stops before a step would cross it and returns what it has, with a resume link.

Emits Audience.harvested. Refuses with max-cost before it spends, and with the Data calls and Accounts groups (refusals).

Contents