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
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' } })
{
"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"
}
}
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.
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.
| Step | Verb | Returns | Price | Approval |
|---|---|---|---|---|
| 1. Find posts that use your problem phrases | Keyword.search | Recent LinkedIn posts on the problem, by comments | About 1¢ | Runs on its own |
| 2. Read who engaged with each post | Prospect.find | The people who commented on or liked each post | About 15¢ | Runs on its own |
| 3. Keep your buyer’s titles and company sizes | Collection.query | The people who pass the declared filter | Free | Runs on its own |
| 4. Read each kept person’s profile | Contact.enrich | Title, employer, seniority and location, current | About 40¢ | Runs on its own |
| 5. Save them as a warm Audience | Audience.create | One Audience, each row with the post it came from | Free | Runs 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
| When | Then |
|---|---|
| A post hides its engagement | It is skipped with engagement-hidden; the read costs nothing. |
| The LinkedIn connection needs signing in again | Only the own-posts part stops, with the fix line; the phrase search still runs. |
| New posts add under 5% new people | In 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 post | The 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
| Ask | Compiles 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 |
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: 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
| Fact | Value |
|---|---|
| Guide | Audience.harvest |
| Guide approval | limits |
| Goal | Find 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. |
| Trigger | on.Post.published((post, |
| Retry | Three times with backoff, then the next route; the same idempotency key never charges, sends or posts twice |
| Idempotency key | leaseline-audience-harvest-1 |
| Lookups | A 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
| Step | Function | Doer | Checker | Assurance | Inputs |
|---|---|---|---|---|---|
| 1. Find posts that use your problem phrases | code | Code | Code checks each result against its declared type | measured | $0.posts.phrases |
| 2. Read who engaged with each post | code | Code | Code checks each result against its declared type | measured | $1.posts, |
| 3. Keep your buyer’s titles and company sizes | code | Code | Code checks each result against its declared type | measured | $2.people, |
| 4. Read each kept person’s profile | code | Code | Code checks each result against its declared type | measured | $3.people |
| 5. Save them as a warm Audience | code | Code | Code reads the result back from where it landed | measured | $4, |
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
Reference
Inputs, outputs, its Event and its errors
| Name | Type | Default | Meaning |
|---|---|---|---|
posts | { phrases? | { own: | 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. |
icp | ICP | your saved ICP | The titles and company sizes kept, as a declared filter. |
take | integer | 200 | People to keep. |
maxCost | number | 1.50 | The 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).