HuluFlow · Node config
Workflows and node configuration
A beginner’s guide: what a workflow is, how to build one in the console, and every important setting for each node type.
If Getting started got you a first scrape, this page explains the full picture. You can build everything in the console; the JSON examples are for people who later use the API. You do not need to memorize field names to use the editor — they are listed here so nothing feels magical. The editor’s AI chat tab can propose graph changes; click Save to persist.
Written for people who have never designed a data pipeline. We explain jargon the first time it appears.
What is a workflow, in plain language?
A workflow is a small assembly line for web data. You place boxes (nodes) on a canvas and connect them with arrows. When the line runs, each box does one job and hands its results to the next box.
Typical jobs: build many page URLs → read each page into rows → save rows into a table → email you when something is new or a price moved. HuluFlow hosts the line in the cloud, so it can run on a schedule while you are offline.
You are not writing a crawler. You configure what to read and where to put it. The product discovers columns from a sample page; you choose which columns to keep.
Before you configure nodes
Have these ready so discovery and runs succeed on the first tries:
- 01A public sample URL you are allowed to collect (no login, no CAPTCHA if possible).
- 02A clear goal: one-time collect into a table, or ongoing monitor with email.
- 03Enough credits on your plan for the rows you expect to scrape this period.
- 04Optional: the pagination pattern of the site (page=1,2,3…) if you need more than one list page.
Mini glossary
These words appear in the editor, emails, and API responses:
Canvas / graph — the drawing of nodes and arrows; stored as JSON behind the scenes.
Upstream / downstream — upstream is the node that runs before; arrows point downstream.
Items — the array of row objects passed between nodes (each row is a dictionary of fields).
Fields / schema — the column definitions (name, label, type) selected after discovery.
Upsert — update a row if the key already exists; insert if it does not.
Credits — 1 credit per webpage request by scrape nodes (not by output row count).
Preset — a reusable scrape, URL generator, or notify config you can drop onto other workflows.
Dry run — notify test mode that shows what would be emailed without sending.
Build it in the console (recommended path)
You can ignore JSON until you need the API. In the UI:
-
01 Create and name the workflow
Workflows → New. Use a name you will recognize in emails later. Leave status paused while editing.
-
02 Add nodes from the palette
Drag scrape, and later store / notify / URL generator. Click a node to open its config on the right.
-
03 Configure scrape first
Paste URL, choose list or detail, write requirement, Fetch fields, tick columns, Apply. Run once and inspect items before adding more nodes.
-
04 Connect with arrows
Drag from an output port to an input port. Direction matters: data flows from → to. URL generator only connects to scrape; scrape connects to scrape/store/notify.
-
05 Add store (and optional notify)
Set dataset name and key_fields (usually url). For notify, pick when=new or field_change. Save the workflow.
-
06 Activate schedule when ready
Set interval_minutes, switch status to active, confirm next_run_at. Keep paused until sample runs look correct.
Workflow-level settings
These settings belong to the whole pipeline, not to a single node. You set them in the workflow header or via the API when creating/updating a workflow.
Runs need remaining credits (saving the canvas does not). HTTP 402 means the pool is exhausted — upgrade or wait for the next period.
| Field | What it means |
|---|---|
name | Human-readable name. Shown in the console list and default notify subject lines. |
status | active = the scheduler may run it when due. paused = scheduler skips it; manual Run still works. Keep paused while experimenting. |
interval_minutes | How often an active workflow should run, in minutes. 1440 ≈ daily, 60 ≈ hourly (plan permitting). Empty/null means manual only. After a run attempt, next_run_at advances by this interval when set. |
Tip: name workflows by business purpose (“Competitor A prices”), not by technical nicknames. Future you will thank present you when the inbox fills up.
How the graph is stored
Visually you see boxes and arrows. Internally HuluFlow stores a graph: a list of nodes and a list of edges. Each node needs a unique id string (the editor creates these), a type, and a config object.
Edges use from and to (node ids). from is upstream. Never use source/target — the API will not understand them. The runner sorts nodes so upstream always finishes before downstream. Loops are rejected with an error.
{
"nodes": [
{ "id": "s1", "type": "scrape", "config": { } },
{ "id": "st1", "type": "store", "config": { } }
],
"edges": [
{ "from": "s1", "to": "st1" }
]
}
If you copy JSON from docs into the API, keep ids stable when you also use notify — notify compares to the previous output of the same node id.
How data moves along edges
Most nodes produce an object that includes items (rows) and often fields (column definitions). The next node receives that as input. If several arrows point into one node, the runner merges available upstream outputs according to the engine rules — in practice, keep graphs simple: one clear path.
URL generator ignores inputs; it only produces URLs. Store and notify look for data rows on their connected upstream (or the first rows available in the run). They do not re-scrape the web.
List→detail is special: the detail scrape reads URLs from each upstream row (via input_field or link/url/href) and merges parent list fields into each detail row so you keep title+price from the list and description from the detail page.
URL generator
Purpose: create many list-page URLs without typing them by hand. It never downloads pages and does not use credits. Think of it as a spreadsheet formula that fills page=1…N.
Output shape is always items: [{url}, {url}, …] with a simple url field. Connect the output only to a scrape node. Cap: 500 URLs per generator per configuration.
When to use it: You need page 1, page 2, page 3… of the same list pattern, or you have a fixed list of seed URLs to paste.
| Field | What it means |
|---|---|
mode | Page range = fill a template with numbers. URL list = paste one URL per line. Default is page range. |
template | range only. Must include the placeholder in braces, e.g. https://shop.example/list?page={page}. Wrong placeholder name → validation error. |
param | Name inside the braces. Default page means the template must contain {page}. If your site uses {p}, set param to p. |
start | range only. First number (inclusive). Default 1. |
end | range only. Last number (inclusive). Must be ≥ start. (end−start)/step+1 cannot exceed 500. |
step | range only. Positive step (default 1). Use 2 for odd pages only, etc. |
urls_text | list only. One absolute http(s) URL per line. Blank lines ignored; invalid URLs fail validation. |
Example configuration
{
"mode": "range",
"template": "https://example.com/list?page={page}",
"param": "page",
"start": 1,
"end": 10,
"step": 1
}
{
"mode": "list",
"urls_text": "https://example.com/a\nhttps://example.com/b"
}
After configuring, use preview/validate in the UI to see how many URLs will be generated before you Apply. Then raise the downstream scrape limit if you generate more than 20 URLs.
Scrape
Purpose: open page(s) and extract structured rows. Each webpage request consumes 1 credit when the workflow runs. Without a working scrape, store and notify have nothing useful.
Two-phase mental model: (1) Discovery — you provide requirement in plain language; the engine proposes fields and a fetch_profile. (2) Extract — later runs use your selected fields (+ profile) to pull those columns repeatedly. Always Apply selected fields before you trust production runs.
list mode returns many items from a listing page. detail mode returns one richer row per URL and can merge fields from an upstream list row. Default limit is 20 URLs per run of this node — raise it when a generator feeds more pages.
When to use it: Any time you need data from a web page. Start with one scrape; add a second scrape only when you need detail pages.
| Field | What it means |
|---|---|
url | Seed URL when nothing upstream provides links. Optional if a URL generator or another scrape feeds URLs into this node. |
mode | List = many rows from a listing page. Detail = one richer row per URL (product/profile). Picking the wrong mode is a common beginner mistake. |
requirement | Plain-language description of columns for discovery, e.g. “title, price, currency, product url”. Not a selector language. |
fields | The columns you kept: [{name, label, type}, …]. Required for scrape presets. Written when you Apply after discovery. |
limit | Max URLs this node processes in one run (default 20). Independent of generator’s 500 cap — both can apply. |
input_field | For list→detail: name of the upstream column that holds the next URL. If empty, the engine tries link, url, href. |
fetch_profile | Internal profile returned by discovery so re-extracts stay stable. The console stores this for you after Fetch fields — you rarely edit it by hand. |
Advanced keys you may see: scope_xpath, item_xpath (from discovery), url_from (force a URL list). Inputs should come from URL generator or scrape — not from store or notify.
Example configuration
{
"url": "https://example.com/list",
"mode": "list",
"requirement": "title, price, url",
"limit": 20,
"fields": [
{ "name": "title", "label": "title", "type": "text" },
{ "name": "url", "label": "url", "type": "url" }
]
}
{
"mode": "detail",
"input_field": "url",
"requirement": "description, sku",
"limit": 20,
"fields": [
{ "name": "description", "label": "description", "type": "text" }
]
}
Credits follow webpage requests. Test the list scrape alone first. If rows look wrong, fix discovery before adding store.
Store
Purpose: save upstream items into a dataset (table) you can browse, export, and query via API. Store does not use a quota slot. It is not offered as a reusable preset because it points at a specific table.
Each incoming row is upserted by hashing key_fields. Same key → update the existing row. New key → insert. That is how daily monitors refresh prices without duplicating products.
When to use it: Whenever you want durable history or exports — almost every serious workflow ends with store.
| Field | What it means |
|---|---|
dataset_id | Numeric id of an existing dataset you own. If set, it wins over dataset_name. |
dataset_name | Create or reuse a table with this exact name under your account. Default falls back to “{workflow name} data”. |
key_fields | Which fields identify a row. Default ["url","link"]. Prefer a single stable product/profile URL. Avoid title-only keys. |
store_fields | Optional allow-list of column names to write. Alias: keep_fields. Omit to store all projected columns from the row. |
Example configuration
{
"dataset_name": "products",
"key_fields": ["url"],
"store_fields": ["title", "price", "url"]
}
After the first successful store, open Datasets to confirm columns and a few rows. Export CSV/JSON from there when you need a spreadsheet.
Notify
Purpose: email you when something changed compared to the previous run of this same notify node. It does not crawl the web; it reads items from upstream (usually scrape or store).
First run often has nothing to compare to — you may get a full set of “new” rows or a baseline with no mail, depending on when. Run twice when testing alerts. Node test supports dry_run (no email sent).
When to use it: Monitoring jobs: new listings, price drops, or any watched field change. Skip notify for pure one-time collects.
| Field | What it means |
|---|---|
when | “When there is new data” = rows not seen last time (matched by link/URL keys). “When watched fields change” = any watched field changes on a matching key. |
email | Recipient address. Defaults to the logged-in account email. |
watch_fields | Field names used with “When watched fields change”, e.g. price. Empty list disables that mode. |
subject | Email subject line. Default includes the workflow name. |
Example configuration
{
"email": "ops@example.com",
"when": "field_change",
"watch_fields": ["price"],
"subject": "[HuluFlow] price change"
}
Wire notify after the scrape (or store) you care about. For price alerts, choose “When watched fields change”, set watch fields to price, and keep a stable URL key upstream.
How to wire nodes (patterns that work)
The editor validates many bad links (for example URL generator cannot feed store directly). Prefer these patterns:
Keep one main spine. Extra branches are possible but harder to debug as a beginner.
- URL generator → scrape (list) — generated URLs become the pages to open.
- Scrape (list) → scrape (detail) — set the link field to the list’s link/URL column; detail merges list + detail fields.
- Scrape → store — rows land in a dataset with upsert keys.
- Scrape or store → notify — notify reads upstream rows; choose “When watched fields change” when you care about value changes on known keys.
Story example: generate catalog pages 1–5 → list scrape (title, price, url) → detail scrape (description) → store keyed by url → notify on price field change. Credits bill by webpage requests.
Limits, quota, and exports
URL generator: max 500 URLs per node. Scrape: default 20 URLs per node per run (configurable via limit). Credits: webpage requests by scrape nodes this period — not “one seat per scrape node”. Out of credits → HTTP 402.
Datasets: console pagination and API page size limits apply when browsing; export supports CSV/JSON up to 100,000 rows (413 if larger). Failed crawls surface as node errors inside the run — open the run detail.
Beginner FAQ
I only want a spreadsheet once. Do I need notify and schedule?
No. Scrape → store, run once, export CSV. Leave status paused and interval empty.
Does pausing free credits?
No — credits are spent on runs, not by keeping a paused workflow. Unused credits remain until the period ends or you upgrade.
Discovery found columns I do not want. Is that bad?
No. Tick only what you need before Apply. Extra discovered columns that you never select are not stored.
List→detail returned rows missing list fields.
Confirm the edge list→detail, detail mode on the second node, and input_field matching the link column. Parent fields merge when the engine can match the detail URL back to the parent row.
Notify never emails.
Check email, when, and watch_fields. Run twice so there is a previous baseline. Confirm the node id did not change between runs. Use dry-run test to see whether the trigger fires.
Should I start with the API?
No. Build and verify in the console first. Use the API when another system must create workflows or pull rows automatically — see the API-first guide.
What next
Continue with a scenario guide, or Concepts for vocabulary.