Founder CRM

Flow files

Getting started

The easiest way to make a flow is the chat in the side panel. Say what you want. It writes the steps. When it looks right, download the markdown file and send it to your team. They import it in Flows.

You can also have an agent write the file. Point it at this page so it uses the right keys.

This guide is that file format: a .md with a recipe between --- lines.

Minimal example

Makers of today's top Product Hunt launch.

---
type: flow // must be flow
format: 1 // leave this as 1
id: ph-top-makers-today // import updates this id
name: "Today's top PH makers" // card title
description: Makers of today's top Product Hunt launch. // card subtitle
start: "https://www.producthunt.com/" // first URL
steps:
  - type: open // go to Product Hunt
    label: Open Product Hunt today
    url: "https://www.producthunt.com/"
  - type: visit_links // open matching hrefs on this page
    label: Open the top product
    urlIncludes: "/products/|/posts/" // homepage product cards
    limit: 1 // top product only
    page:
      - type: click // runs on that product page
        label: Expand Launch Team
        text: Show more
      - type: extract // add people from this page
        label: Add Launch Team makers
        collect: dom
        instruction: Launch Team makers of this product only. Skip Hunter-only.
heal:
  expectMinPeople: 1 // fail the run if nobody is added
  success: Makers from today's top Product Hunt launch.
  notes: Homepage cards are /products/ or /posts/. Launch Team starts collapsed.
---

Import the file in Flows. The card title comes from name. The subtitle comes from description.

What goes in the file

Every file needs type: flow, format: 1, id, name, start, and a steps list. Each step needs type and label. Unknown keys fail import.

start is the first URL. {{id}} pulls a value you type when the flow runs.

steps is the recipe. Most people jobs are a short list: get to the right pages, add people, drop the misses, then read X.

heal is for people scrapes. It says what a good run looks like. Skip it when you only click (upvote, and so on).

Ready-made files you can import are on Templates.

How it works

Write steps in this order. Keep the list short.

  1. Browse: get to the right page
  2. Add: put people on the list
  3. Filter: drop people who fail a check
  4. Enrich: fill fields from a profile
  5. Act: click a button, like upvote

One Add, then Filter while you peek, then Enrich. Do not hide a scrape plan in instruction. That field is who to keep on the page.

Browse

Get onto the page the rest of the recipe needs.

  • open goes to a URL. First real step in most files.
  • click clicks a button or link (text: Show more, or a selector).
  • browse looks around like a person. Use it before an action so the click does not fire on a cold load.
  • scroll jumps the viewport to load more. Prefer paginate on X Latest instead of chaining this to collect people.
  • visit_links without a page extract opens matching hrefs and stops there.
- type: open
  label: Open Product Hunt today
  url: "https://www.producthunt.com/"

wait is a dead pause. Prefer browse when the job should look human.

Add

Put people on the list from a page you can already see, or from pages you open on purpose.

  • extract reads the current page. collect: dom is the default. Add instruction so it knows who counts (Launch Team makers, tweet authors).
  • paginate walks several X Latest viewports and adds authors as it goes.
  • visit_links snapshots the current page, keeps hrefs that match urlIncludes, opens the first limit unique pages, then runs page on each tab.
- type: visit_links
  label: Add makers from top products
  urlIncludes: "/products/|/posts/"
  limit: 8
  page:
    - type: click
      label: Expand Launch Team
      text: Show more
    - type: extract
      label: Add Launch Team makers
      collect: dom
      instruction: Launch Team makers of this product only. Skip Hunter-only.

limit on visit_links is pages, not people. Do not copy that 8 onto visit_profiles.

Work that should run on every opened page belongs under page. A sibling step after visit_links runs once, on whatever tab is left.

Filter

Drop people who fail a check. The list updates as they fail.

  • keep + urlIncludes keeps a person if their url, citation, or profile links match (twitter.com|x.com).
  • keep: can_dm keeps people with Message next to Follow on x.com.
  • keep: match is a judgment call. It needs instruction ("complaints", "worth a reply").

Put keep under visit_profiles.page when the check needs a peek (Links on a PH profile, Message on X). A top-level keep only uses data already on the person.

- type: visit_profiles
  label: Keep people with X
  urlIncludes: producthunt.com/@
  page:
    - type: extract
      label: Read Links
      collect: dom
    - type: keep
      label: Keep if Twitter or X is listed
      urlIncludes: twitter.com|x.com

has_x is an old alias. Write urlIncludes: twitter.com|x.com instead.

Enrich

Open each collected person and fill the card (photo, bio, followers, website).

- type: visit_profiles
  label: Read the X profile
  urlIncludes: x.com
  page:
    - type: extract
      label: Read the X profile
      collect: dom

No keep means everyone stays. urlIncludes here selects people you already have, not hrefs on the current page.

Do not add wait or browse on every x.com profile. The peek already waits for the photo.

Act

Click a button or link on the page. Point at it with the words on screen (text: Show more) or a CSS selector. Reddit upvote is script: reddit-upvote.

- type: click
  label: Upvote
  script: reddit-upvote
  text: upvote

Action recipes often skip people entirely. Omit heal when you do not collect anyone.

Repeat over inputs

inputs are values you type each run. each runs the whole steps list once per list item. {{id}} substitutes into start and into step url, text, selector, and instruction.

inputs:
  - id: link
    type: list
    itemType: url
    label: Reddit links
    placeholder: "https://www.reddit.com/r/…"
each: link
start: "{{link}}"

typeurltextnumberselectlist

each is not a step. Do not put it on a step. page is the nest for links and people.

page is a short playbook that runs on every opened href or person. One level only. Allowed inside page: click, extract, browse, wait, scroll, keep.

urlIncludes always matches the list that step walks.

StepWhat it matches
visit_linksHrefs on the current page
visit_profilesCollected person.url / citationUrl
keepThose person fields after the peek

Homepage cards on Product Hunt are /products/ or /posts/. PH @ urls appear after you extract makers. visit_links with urlIncludes: producthunt.com/@ on the homepage finds nothing. visit_profiles with urlIncludes: /products/ also finds nothing. That step does not read the page.

A people scrape

Today's Product Hunt makers you can DM.

---
type: flow
format: 1
id: ph-makers-i-can-dm
name: Today's PH makers I can DM
description: Makers of today's top PH launches who have X and open DMs.
start: "https://www.producthunt.com/"
steps:
  - type: open
    label: Open Product Hunt today
    url: "https://www.producthunt.com/"
  - type: visit_links
    label: Add makers from top products
    urlIncludes: "/products/|/posts/"
    limit: 8
    page:
      - type: click
        label: Expand Launch Team
        text: Show more
      - type: extract
        label: Add Launch Team makers
        collect: dom
        instruction: Launch Team makers of this product only. Skip Hunter-only.
  - type: visit_profiles
    label: Keep people with X
    urlIncludes: producthunt.com/@
    page:
      - type: extract
        label: Read Links
        collect: dom
      - type: keep
        label: Keep if Twitter or X is listed
        urlIncludes: twitter.com|x.com
  - type: visit_profiles
    label: Keep people I can DM
    urlIncludes: x.com
    page:
      - type: extract
        label: Read the X profile
        collect: dom
      - type: keep
        label: Keep if I can DM
        keep: can_dm
heal:
  expectMinPeople: 2
  success: Makers from today's top launches who have X and a Message button.
  notes: Homepage cards are /products/ or /posts/. X lives under Links on /@ profiles.
---

Browse the homepage, Add makers from the top product pages, Filter to people with X, then Filter again to open DMs. The second visit_profiles is also Enrich: the peek fills the X card.

An action

Upvote one or more Reddit links. No people, no heal.

---
type: flow
format: 1
id: reddit-upvote
name: Upvote a Reddit link
description: Paste Reddit posts or comments. Open each, then upvote.
start: "{{link}}"
each: link
inputs:
  - id: link
    type: list
    itemType: url
    label: Reddit links
    placeholder: "https://www.reddit.com/r/…"
steps:
  - type: open
    label: Open the post
    url: "{{link}}"
    rewrite: reddit-thread
  - type: browse
    label: Look around
    script: human
    ms: 30000
    ready: shreddit-post
  - type: click
    label: Upvote
    script: reddit-upvote
    text: upvote
---

rewrite: reddit-thread opens the post page and keeps the comment id.

Before you import

  • Every step has type and label.
  • format: 1.
  • description is card copy (140 characters). Not heal.success.
  • Loops use page or each, not a sibling click you meant to run N times.
  • urlIncludes matches the list that step walks.
  • People scrapes have heal.expectMinPeople.
  • No wait or browse on every x.com profile.
  • Do not invent fields. Unknown keys fail import.

Reference

A flow file is a .md with a recipe between --- lines. Import also accepts JSON with the same keys. Unknown keys fail import.

The file

---
type: flow
format: 1
id: my-flow
name: Short title
description: One or two lines for the card. 140 characters max.
start: "https://example.com/"
steps:
  - type: open
    label: Open the page
    url: "https://example.com/"
---
KeyRequiredWhat it is
typeyesMust be flow.
formatyes on new filesMust be 1.
idyes to updateImport upserts this id.
nameyesCard title.
descriptionyes for people jobsCard copy. Not heal.success.
startyesFirst URL, or {{token}}.
inputsnoValues you type each run.
eachnoInput id. Run steps once per list item.
stepsyesOrdered recipe.
healpeople scrapesWhat a good run looks like.

description can also sit in the body after the closing ---. Frontmatter wins.

Inputs

inputs:
  - id: link
    type: list
    itemType: url
    label: Reddit links
    placeholder: "https://www.reddit.com/r/…"
each: link
start: "{{link}}"

typeurltextnumberselectlist

itemType on a listurltext

options on select: a string array.

{{id}} substitutes into start and step url / text / selector / instruction.

Heal

heal:
  expectMinPeople: 2
  success: What a good people scrape looks like.
  notes: What to try if it drifted.

Required for people scrapes. Omit for action recipes. Never use success as the card description.

Steps

Every step needs type and label. Omit empty keys. One level of page only. page steps cannot nest another page.

Allowed page typesclickextractbrowsewaitscrollkeep

open

Go to a URL.

KeyWhat it is
urlFull https URL, or {{token}} from an input.
rewritereddit-thread: open the post page, keep the comment id.

First real step after an optional interpret.

wait

Dead pause. Default 800ms. Prefer browse when the job should look human.

KeyWhat it is
msMilliseconds.

browse

Look around like a person (inertia swipes, reading pauses).

KeyWhat it is
scripthuman.
msHow long (default 30000).
timesOptional swipe cap.
readyCSS selector that means the page is ready.
selectorOptional target to work toward.

scroll

Fast viewport jumps to load more. Use paginate on X Latest instead of chaining this to collect people.

KeyWhat it is
timesViewports (1 to 8).

click

Click a button or link.

KeyWhat it is
textVisible label (PH: Show more).
selectorCSS selector.
scriptreddit-upvote for Reddit votes.

A top-level click runs once. Under visit_links.page it runs on every opened href.

extract

Add people from the current page, or peek the current profile when nested under visit_profiles.page.

KeyWhat it is
collectdom (default) or ai. Prefer dom.
instructionWho to keep on the page. Required for ai.
urlIncludesOn a lone extract: only keep people whose url matches.

paginate

Add people across several X Latest viewports.

KeyWhat it is
timesPages (3 to 8).
limitStop after this many people.
collectUsually dom.

Map over hrefs on the current page.

  1. Snapshot the page.
  2. Keep hrefs that match urlIncludes, first limit unique pages.
  3. Open each href.
  4. Run page on that tab.
KeyWhat it is
urlIncludesHref needles, `\` separated. Page snapshot only.
limitPages to open, not people. 5 to 8 for top products.
pagePer-href playbook.

Old shorthand: text / collect / instruction if page is omitted. Prefer page.

visit_profiles

Map over collected people.

  1. Take people whose url / citationUrl match urlIncludes.
  2. Open each person’s url.
  3. Run page (extract peeks, keep drops as you go).
KeyWhat it is
urlIncludesPerson urls, not the current page.
pagePer-person playbook.
limitOmit. Default is everyone (cap 80).

Old shorthand: keep (can_dm, has_x) if page is omitted. Prefer page keep.

keep

Filter people already on the list.

KeyWhat it is
urlIncludesKeep if person url, citationUrl, or profile links match.
keepcan_dm: Message next to Follow on x.com. match: needs instruction. has_x: old alias for twitter/x urlIncludes.

Put keep on visit_profiles.page when the check needs a peek. A top-level keep only uses data already on the person.

interpret

Optional first step. Reads a prompt from the trigger URL and fills inputs. Needs instruction. Do not use as the default.

Named values

A few keys take a fixed word. Use text, selector, or urlIncludes when a visible label or URL substring is enough.

KeyValues
script on browsehuman
script on clickreddit-upvote
rewrite on openreddit-thread
keep on keepcan_dm, match (has_x is an old alias)
collectdom, ai