Fourteen step types exist. Thirteen have an entry in the step palette and can be added to a workflow today; one runs in the engine but has no way into the builder. The identifier in the first column is what the Type column shows when you inspect a run.
In the palette
| Step type | Palette name | What it does | Complete when |
|---|---|---|---|
http_request |
HTTP Request | Calls any HTTP endpoint and passes the response to the steps below. | It has a URL and a method. |
api_request |
API Request | Calls one operation from an imported API Spec, with its parameters typed for you. | An API Spec and an operation are selected. |
harmonizer |
Map to Data Model | Maps incoming fields onto a data model and saves the result to the warehouse as a record. | A data model is selected and at least its primary key is mapped. |
filter_list |
Filter List | Keeps only the items of an upstream list that match a condition, and skips the branch below when nothing survives. | It has a condition. |
conditional |
Conditional | Evaluates its branch conditions top to bottom and sends the run down the first one that matches, or down Default. | Every branch has a condition and a label. |
subflow |
Subflow | Runs another workflow as a run of its own and continues with whatever it returns. | An active subflow workflow is selected. |
data_transform |
Data Transform | Reshapes, renames, and computes fields with formulas before the next step reads them. | At least one output field is mapped. |
s3_read |
S3 Read | Downloads one object, or lists objects under a prefix, from S3-compatible storage. | A connector is selected, and a download has an object key. |
s3_write |
S3 Write | Writes one object to S3-compatible storage. | A connector is selected and it has an object key. |
snowflake_read |
Snowflake Read | Runs a SELECT against Snowflake and passes the rows on. |
A connector is selected and it has a query. |
snowflake_write |
Snowflake Write | Writes rows into a Snowflake table, by insert, merge, or replace. | A connector is selected and it has a target table. |
send_email |
Send Email | Emails a short notification from Hodoflow's own address, with no connector to set up. | It has at least one recipient, a subject and a message. |
Data Transform and Map to Data Model share one mapping panel. The difference is the right-hand column: a Map to Data Model step fills in a data model's fixed fields and saves a record, a Data Transform step names its own output fields and passes them to the next step. See Use fields from an earlier step.
The four steps that reach an outside service — S3 Read, S3 Write, Snowflake Read and Snowflake Write — each need a connector of a matching kind before they will run. The two that send data also choose what to send: leave Input blank to send the payload that started the run, or name an earlier step to send what that step produced.
The fields naming what a step touches — an object key or prefix, a table name,
a query's bind values — accept {{field}} references, so a run can
choose where it writes. Leave one fixed and every run writes to the same place
and overwrites what was there.
A Snowflake Read is the exception: its query is sent as written and takes no references, because a value from an earlier step spliced into SQL would be read as SQL. Anything that changes from run to run goes in its Parameters instead, where a line that is nothing but a reference keeps that value's type.
In the engine only
This executes normally in a run. It has no palette entry, so it can only reach a workflow through a workflow built elsewhere.
| Step type | What it does |
|---|---|
delay |
Parks the run until a fixed instant and frees the slot until then. The deadline is recorded when the run suspends, so a resume never restarts the clock. |
Reusing a workflow as a subflow
A Subflow step runs another workflow — one saved with type subflow — as a run of its own, and continues with whatever it returns.
Give the subflow parameters
Open the subflow and click the Subflow card at the top of its canvas — the one reading Start of this subflow. Its Inputs panel lists the values a calling workflow passes in. Add parameter opens a dialog asking for:
| Field | Notes |
|---|---|
| Name | What you read — "Discord channel". |
| API name | How the subflow's steps refer to it: {{params.channel_id}}. Filled in from the name, and editable. Letters, numbers and underscores, starting with a letter. |
| Type | What a supplied value is converted to before the subflow sees it. |
| Default value | Used when a caller passes nothing. |
| Description | Shown beside the field in the caller's inspector. |
Save is refused until the parameter is valid, and the same rules are checked again when the workflow is saved and when it is activated — so a parameter that no step could resolve cannot go live.
The two names exist for the reason a step has both a name and an API Name: the readable one is free to change without breaking every reference to it.
In the workflow that calls this subflow, the Subflow step's inspector shows one
field per parameter, and each takes a literal or a {{field}} from a step above
it.
This is what makes a subflow reusable. Without parameters a subflow receives the
calling workflow's entire context — its trigger and every step above the
Subflow step, under that workflow's own step names — so a subflow reading
fetch_orders.data works under a workflow with a step called fetch_orders and
quietly reads nothing under one that called the same step orders.
Declaring the first parameter closes that door. From then on the subflow receives only what was passed to it. A subflow that declares no parameters keeps receiving everything, so nothing you already built changes until you choose to change it — and because adding parameters saves a new version, the version already running keeps running until you activate the new one.
Nothing is required
A parameter is never mandatory, and adding one never breaks a workflow that already calls the subflow. It cannot: a calling workflow can only offer the parameters of the version that is live, so a parameter is invisible to every caller until the moment it ships.
A caller that passes nothing gets the parameter's default, and a parameter with no default is simply empty inside the subflow. Give a default to anything the subflow cannot work without — it is the only thing standing between a caller that has never heard of a parameter and an empty value.
Say what it gives back
The same card has an Outputs panel, and it is the other half of the same idea. Without it, a workflow calling this subflow receives the subflow's last steps, under those steps' own API names — so it reads results by reaching inside. Rename a step in the subflow and every workflow calling it quietly stops finding anything, with no warning at either end.
Add output asks for:
| Field | Notes |
|---|---|
| Name | What you read — "Order ID". |
| API name | How a calling workflow reads it: {{this_step.order_id}}. |
| Value | Where it comes from inside this subflow — any step, not only the last ones. |
| Type | What the value is converted to before the caller sees it. |
| Description | Shown beside it in the caller's field picker. |
Declaring the first output replaces the raw result. From then on a caller receives exactly the list you named and nothing else, and the steps in here are free to change as long as those names keep their meaning. A subflow declaring no outputs keeps handing back its last steps, so nothing you already built changes until you choose to change it — and because adding outputs saves a new version, the version already running keeps running until you activate the new one.
Renaming a step in this subflow rewrites the outputs that read from it, so the promise survives the edit. An output left pointing at a step that no longer exists stops the workflow being activated.
In the calling workflow, the Subflow step's inspector lists what comes back under Returns, and those names are what its field pickers offer to every step below it.
Trying it out
The Subflow step's inspector has Run with sample input: fill in the same parameters with stand-in values and press Run sample. The subflow runs immediately, start to finish, and shows what it returned — the declared outputs if it has any, otherwise its last steps raw. Nothing is recorded, so it does not appear in Monitor.
Which types need a connector
| Step type | Connector |
|---|---|
http_request |
Optional. An outbound HTTP connector supplies the base credentials. |
api_request |
Supplied by the API Spec's connector. |
send_email |
None — it sends from Hodoflow's own address. |
snowflake_read, snowflake_write |
Required — a Snowflake connector. |
s3_read, s3_write |
Required — an Amazon S3 connector. |
| Everything else | None. |
See What you need before you build.
Sending email
Send Email exists so a new workflow can tell somebody something on its first day, before any mail provider is connected. It sends from Hodoflow's own address, which is what lets it work with no setup — and what makes it a notification channel rather than a mailing tool:
- Up to 10 recipients per step. Each person gets their own copy and cannot see the others.
- It cannot run once per item, so it can't be pointed at a list of customers.
- Messages are plain text. A blank line starts a new paragraph, and anything that looks like HTML is shown as written.
- Recipients, subject and message all accept
{{field}}references. - Sends count against your plan's Notification emails allowance for the month.
For mail your customers see — your own domain, your own templates, real volume — connect SendGrid, Mailgun or Postmark as an outbound HTTP connector and use an HTTP Request step. See What you need before you build.
Which types can run once per item
HTTP Request, API Request, Data Transform, and Subflow have an Iteration section that walks an upstream list and runs the step once per element. See Configure and test a step.
Which type writes to the warehouse
Only Map to Data Model. Everything else moves data around; that is where data lands and stays. See Persist workflow data.