# Step types

> Every step type Hodoflow runs, what it does, whether it can be added from the palette, and what it needs to be complete.

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](/help/workflows/map-upstream-fields).

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](/help/credentials-connections/prerequisites).

## 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](/help/credentials-connections/prerequisites).

## 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](/help/workflows/configure-and-test-steps).

## 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](/help/data-dashboards/persisting-data).
