> For the complete documentation index, see [llms.txt](https://docs.notionapps.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.notionapps.com/automation/advanced-reference/step-catalog.md).

# Step catalog

Canonical list of Workflow Foundation steps. How-tos stay short and link here. Screen guides do not copy this page.

In the builder, steps live on **Build** under **Add a step**. Each tile adds one node. Click a node to fill its fields in the inspector. Drag to reorder. The builder rewrites `next_step_id` for a linear path.

A step is three things:

1. **Step** — the kind of work (create a row, send email, wait, open a screen).
2. **Config** — the fields for that type (data source, recipients, screen, topic, delay).
3. **Place in the list** — live-screen steps only update the open tab if they run before background work (Create, Notify, Wait, Publish message). **Call webhook and wait**, **Update record**, **Change status**, and **Assign user** can stay inline when **Live screen updates** is on for the app.

Use the smallest step that does the job. Do not add **Condition** plus **Branch** when one Condition is enough. Do not use **Notify → Webhook** when Make must refresh the tab the user already has open — that is **Call webhook and wait**.

## What this is / when to use it

A step is one node in a run. Type enum: `WorkflowStepType`. Each step has `id`, `name`, `config`, `next_step_id`, optional `on_error_step_id`, and optional `branches`. Successful output is stored as `context.<step_id>` for later steps.

## Before you start

Workflow entitlement is on. The trigger is set. Notion properties you write are writable. Screens, buttons, and Messaging channels you name exist. You have read [Which automation](https://docs.notionapps.com/automation/which-automation) and the [Trigger catalog](https://docs.notionapps.com/automation/advanced-reference/trigger-catalog).

## Build it

1. Open **Automation → Workflows** and select the workflow (or start from a wizard).
2. On **Build**, click an **Add a step** tile.
3. Name the step in plain language (`Create follow-up task`, not `Step 2`).
4. Fill the inspector fields below. Use **Event data** for `payload.*` / `context.*` and **Fixed value** for literals.
5. Put live-screen steps (Refresh, Show message, Open screen) before Create / Notify / Wait when the open tab must update.
6. Save, publish the workflow, and publish the app. Confirm the run on **Activity**.

## Every control

These are the **Add a step** tiles on **Build**:

| Add a step             | Enum                     | What it does                                                                 | Main fields                                           | Do not use when                                                           |
| ---------------------- | ------------------------ | ---------------------------------------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------- |
| Create record          | `create_notion_record`   | Adds a new row to a connected database                                       | Data source, field mappings                           | You meant to change the row that already triggered the run                |
| Update record          | `update_notion_record`   | Changes fields on an existing row                                            | Data source, record path, field mappings              | You do not have a record id (`payload.record_id` or earlier step output)  |
| Create related records | `create_related_records` | Creates several child rows linked to the trigger record                      | Related data source, relation field, labels           | The related sheet has no relation property pointing at the parent         |
| Notify                 | `send_notification`      | Email, in-app Notification Center, or fire-and-forget webhook                | Channel, recipients or endpoints, subject, body       | Make must return JSON and refresh the open tab                            |
| Call webhook and wait  | `call_webhook_and_wait`  | POSTs Make (or another URL), waits for JSON, then continues on the same tab  | Webhook URL, sync timeout, optional headers/body      | You only need a fire-and-forget POST — use Notify → Webhook               |
| Open screen            | `open_screen`            | Moves the current user to another screen                                     | Screen, presentation, optional record path            | The user is not in a live tap (scheduled / webhook-only run)              |
| Show or hide control   | `set_control_visibility` | Shows or hides one control this session                                      | Control, visibility, optional screen                  | You need a permanent builder change — edit the screen instead             |
| Set control text       | `set_control_text`       | Replaces visible text this session                                           | Control, text source, optional screen                 | You need to write a Notion property — use Update record                   |
| Refresh data           | `refresh_screen_data`    | Reloads rows on a screen                                                     | Optional screen, optional data source                 | Nothing wrote data yet — put Update / Change status first                 |
| Show message           | `show_message`           | Short toast on the current screen                                            | Message, tone                                         | You need email — use Notify                                               |
| Stop action            | `block_interaction`      | Stops default button/form follow-through                                     | Optional message                                      | The save already completed and you only wanted a toast — use Show message |
| Publish message        | `publish_message`        | Publishes on Application Messaging; can create a Decision or Work Queue card | Channel, topic, inbox item, routing                   | You do not have Messaging entitlement                                     |
| Send request           | `send_message_request`   | Publishes a request and parks the run until a reply                          | Channel, topic, routing, timeout                      | You only need a one-way announce — use Publish message                    |
| Send reply             | `send_message_reply`     | Replies to the triggering message or correlation                             | Topic, reply status, payload                          | This run was not started by a message / request                           |
| Wait for message       | `wait_for_message`       | Pauses until a matching topic arrives                                        | Channel, primary topic, wake-on topics, timeout       | You only need a clock delay — use Wait                                    |
| Condition              | `condition`              | Checks payload or earlier output before continuing                           | Rules (field, operator, value)                        | You need several named paths — that is Branch (templates / API)           |
| Wait                   | `wait`                   | Pauses for a duration or until a time                                        | Delay seconds or Wait until, optional escalation step | The next step must happen in the same tap — put it before Wait            |
| End                    | `end`                    | Marks the path complete                                                      | None                                                  | Optional; the last step with no next step also finishes                   |

Also used by recipes and templates (not separate **Add a step** tiles):

| Step          | Enum            | What it does                                 | Main fields                                             |
| ------------- | --------------- | -------------------------------------------- | ------------------------------------------------------- |
| Change status | `change_status` | Writes a status/select on the current record | Data source, record path, field mappings (status value) |
| Assign user   | `assign_user`   | Writes an assignee / person field            | Data source, record path, field mappings (user)         |
| Branch        | `branch`        | Multi-way split with labeled conditions      | Rules / branch labels                                   |

**Live screen updates** (Workflow Operations → Controls) is off by default. NotionApps turns it on per app. When it is on, Update / Change status / Assign user / Call webhook and wait can Refresh the tab the user already has open.

***

## Each Add a step tile

The labels below are the ones on **Build**. Hidden/internal screens can appear in pickers because workflows can send people there on purpose.

### Create record

**Step:** Create record (`create_notion_record`)

Adds a **new** page to a connected Notion database or app sheet. It does not edit the row that started the run.

**Fields**

* **Data source** — the database that should receive the new row. Search by name. The builder saves `sheet_id`.
* **Field mappings** — destination field plus **Event data** (`payload.name`) or **Fixed value** (`Open`).

Output later steps can read: `context.<step_id>.record_id`, `context.<step_id>.sheet_id`.

**Example:** Intake submitted → Create record on `Tasks` → Task title = `payload.name`, Status = `Open`.

**What users see:** Nothing from this step itself. They see a later Show message, email, or a new row after they refresh.

**Do not use** to change the triggering row. That is **Update record**. This step is background work: put Refresh / Show message **before** it if the open tab must update first.

### Update record

**Step:** Update record (`update_notion_record`)

Changes fields on an **existing** row. The default record is `payload.record_id` (the form or button’s current record).

**Fields**

* **Data source** — the database that owns the row.
* **Record reference path** — usually `payload.record_id`, or `context.<earlier_step>.record_id` after Create record.
* **Field mappings** — what to write.

Output: `context.<step_id>.record_id`, `sheet_id`, `updated_count`.

**Example:** Reviewer clicks `Mark reviewed` → Update record on `Requests` → Status = `Reviewed`, Reviewed by = `payload.user_email`.

**What users see:** The Notion row changes. The open tab updates only if **Refresh data** follows this step **and** Live screen updates is on (or this is still in the inline path).

**Do not use** without a record id. A completed run with `updated_count: 0` wrote nothing. If a later step writes the same sheet, a **Record updated** trigger on that sheet can loop — add a Condition or use **Status changed**.

### Create related records

**Step:** Create related records (`create_related_records`)

Creates **several child rows** linked to the trigger record — photo checklist slots, punch-list items, one row per label.

**Fields**

* **Related data source** — the child database (for example `Photos`).
* **Relation field (link to parent)** — the relation/reference on the child that points at the parent.
* **Label / name field** — text field that receives each label (`Roof`, `Exterior`).
* **Checklist labels** — one related row per label. Enter to add; click a chip to remove; or paste many lines.
* **Optional parent data source** — after children are created, write the parent (for example Status → `Scheduled`).
* **Optional notes field** + **Notes text** — the same static text on every child.

**Example:** Assign Job form submitted → Create related records on `Photos` → relation = Job, labels = Roof / Exterior / Electrical.

**What users see:** New child rows after sync. Not a toast unless you add Show message.

**Do not use** if the child database has no relation to the parent. Do not also fire **Record created** on the child sheet for the same write unless you want a second workflow per slot.

### Notify

**Step:** Notify (`send_notification`)

Sends **email**, an **in-app** Notification Center item, or a **fire-and-forget webhook POST**. It does not wait for Make to finish.

**Fields**

* **Channel** — Email (default), In-app, or Webhook. SMS is stubbed and fails validation.
* **Preference category key** — optional; honors App audience & notification prefs.
* **Bypass preference policy** — only for critical/system mail.
* **Recipients** or **Endpoints** — emails, or HTTPS URLs when Channel is Webhook.
* **Subject** and **Body** — text or `{{payload.fields.fieldId}}` tokens. For webhook, body is a field inside the posted JSON.

Output: `channel`, `recipient_count`, `sent`, `delivery_status` (`sent` or `simulated` in local dev).

**Example:** Form submitted → Notify → Email `reviewer@example.com` → subject `New intake received`.

**What users see:** An email, or a Notification Center item. Webhook recipients see nothing in the app.

**Do not use** when Make must return JSON and Refresh the **same** tab. That is **Call webhook and wait**. Notify after Create/Wait is background-only.

### Call webhook and wait

**Step:** Call webhook and wait (`call_webhook_and_wait`)

POSTs an HTTPS URL (usually Make), **waits for a JSON Webhook response**, then later steps (Refresh, Open screen, Show message) can run on the **same open tab**.

**Fields**

* **Webhook URL** — Make (or other) hook URL. Make’s scenario must **end with Webhook response** JSON.
* **Sync timeout (ms)** — default `8000`, max `12000`. After \~8s the portal can keep waiting in the background (\~45s). A 4xx/5xx in the window errors and does not refresh.
* **Headers JSON** — optional custom headers.
* **JSON body** — defaults to the trigger payload. Response JSON is `context.<step_id>` for later Open screen.

**Example:** Approve button → Call webhook and wait → Make looks up pricing → Webhook response → Refresh data → Show message `Updated`.

**What users see:** Working… on the tab, then the refresh/message **if** Live screen updates is on and Make returned JSON. If Make never responds, they are asked to refresh.

**Do not use** for “just email someone.” Use Notify. Do not use Notify → Webhook for this job. Inbound webhooks Make fires **at** NotionApps still cannot refresh an already-open tab.

Details: [Webhooks and Integrations](https://docs.notionapps.com/automation/webhooks-and-integrations), [Workflow vs Messaging Webhooks](https://docs.notionapps.com/automation/webhooks-and-integrations/workflow-vs-messaging-webhooks).

### Open screen

**Step:** Open screen (`open_screen`)

Navigates the **current user** to another app screen during the same interaction.

**Fields**

* **Screen** — search by name. Saves `screen_id`. Hidden/internal screens can appear.
* **Presentation** — Primary navigation (replaces the stack) or Modal (opens over the current screen).
* **Record reference path** — optional. Default `payload.record_id`, or `context.<create_step>.record_id`.

**Example:** Submit for review → Open screen → Review detail, record = `payload.record_id`.

**What users see:** The destination screen, if this step runs in the inline path.

**Do not use** on a scheduled or inbound-webhook run with nobody looking. Put this **before** Create, Notify, or Wait, or after Call webhook and wait when Live screen updates is on.

### Show or hide control

**Step:** Show or hide control (`set_control_visibility`)

Shows or hides one control **for this session**. It does not change the published screen definition.

**Fields**

* **Control** — button, input, or display. Suggestions include type and screen.
* **Visibility** — Show control or Hide control.
* **Screen** — optional. Blank = current / last opened screen.

**Example:** Priority is High → Show or hide control → show `Reviewer notes`.

**What users see:** The control appears or disappears on the open screen (inline path only).

**Do not use** to permanently remove a field. Edit the screen. Put this before background steps.

### Set control text

**Step:** Set control text (`set_control_text`)

Replaces the **visible text** of a label, heading, button, or display control for this session.

**Fields**

* **Control** — the control whose text should change.
* **Text source** — Event data (`payload.*`) or Fixed text.
* **Screen** — optional. Blank = current screen.

**Example:** After submit → Set control text on a status heading → `Submitted`.

**What users see:** New copy on that control (inline path only).

**Do not use** to write a Notion property. That is Update record. This never persists after a full reload unless something else wrote the data.

### Refresh data

**Step:** Refresh data (`refresh_screen_data`)

Tells the live app to **reload** rows it already knows how to show. It writes nothing.

**Fields**

* **Screen** — optional. Blank = current screen.
* **Data source** — optional. Use when only one sheet should reload.

**Example:** After Update record or Call webhook and wait → Refresh data → current details screen.

**What users see:** Updated values on the open tab when Live screen updates is on (or the step is still inline). When the switch is off, Refresh after Update cannot update the tab they already have open.

**Do not use** as the first step. Put the write (Update, Change status, Assign user, or Call webhook and wait) first. Create / Notify / Wait before Refresh makes Refresh background-only.

### Show message

**Step:** Show message (`show_message`)

Shows a short toast on the current screen. Not email.

**Fields**

* **Message** — keep it short.
* **Tone** — Info, Success, Warning, or Error.

**Example:** After approve + Refresh → Show message `Request approved.` (Success).

**What users see:** The toast, if this step is still inline.

**Do not use** for email or Notification Center. That is Notify. Put this before Create / Notify / Wait, or after Call webhook and wait.

### Stop action

**Step:** Stop action (`block_interaction`)

Stops the app’s **default** follow-through for this tap (button behavior, post-submit redirect/clear).

**Fields**

* **Message** — optional explanation shown when the action stops.

**Example:** Priority missing → Condition fails path → Stop action → `Choose a priority before sending.`

**What users see:** The default navigation does not happen; they may see the message.

**Do not use** after a form save if you only wanted a toast — the row is already saved. Use Show message. For button clicks it prevents the normal button behavior. Inline path only.

### Publish message

**Step:** Publish message (`publish_message`)

Publishes on **Application Messaging**. Can create a Decision or Work Queue card. This is not Notify email and not SMS.

**Fields**

* **Channel** — app Messaging channel, or Custom….
* **Topic** — presets such as `client.review.requested` (Decision) and `client.intake.submitted` (Work Queue).
* **Create inbox item** — None, Task, or Decision.
* **Recipient routing** — Decision audience/tenant, role pool, user field, or a specific email.
* **Title / Message / Summary** — guided copy; `{{payload.path}}` tokens allowed. Payload JSON is advanced.
* **Preference category** — same idea as Notify.

Requires Messaging entitlement.

**Example:** Status becomes `With Client` → Publish message → topic `client.review.requested` → Decision card for the client audience.

**What users see:** A Decision, Work Queue, Conversation, or Notification Center item when those screens exist — not an email unless you also Notify.

**Do not use** if you only need email. Use Notify. This is background work for the open tab.

Topic names: [Topics that perform work](https://docs.notionapps.com/automation/topics-that-perform-work).

### Send request

**Step:** Send request (`send_message_request`)

Publishes a **request** on Messaging and **parks the run** until a reply or timeout.

**Fields**

* Same channel / topic / inbox / routing / title fields as Publish message.
* **Timeout** — default one hour (`timeout_seconds`).
* Reply-to channel when you need a specific return path.

**Example:** Send to Needs Review → Send request → wait for `client.decision.approve` / `changes_requested` / `reject`.

**What users see:** The Decision / queue item. The run shows **waiting** on Activity until they reply.

**Do not use** for a one-way announce. That is Publish message. After resume, use Condition on `context.__workflow_rehydration.payload.decision`.

### Send reply

**Step:** Send reply (`send_message_reply`)

Publishes a **reply** to the triggering message or configured correlation.

**Fields**

* **Topic** — often a decision reply preset.
* **Reply status** — for example `completed`.
* Payload (guided or JSON).

**Example:** Reviewer approves on a Decision screen → Send reply → `client.decision.approve` so the waiting request run wakes.

**What users see:** The waiting request continues; the requester may see the next step (status change, email).

**Do not use** on a form-submit workflow that never received a message.

### Wait for message

**Step:** Wait for message (`wait_for_message`)

Pauses the run until a matching **channel/topic** arrives (or timeout). Use after Publish message / Send request, or when another app will POST to messaging intake.

**Fields**

* **Channel**
* **Primary topic**
* **Wake on any of these topics** — multi-select (`topics[]`). Decision presets: `client.decision.approve`, `changes_requested`, `reject` (and supervisor variants).
* **Correlation** — default `context.correlation_id`.
* **Timeout seconds** — default `3600`.

**Example:** Publish `client.review.requested` → Wait for message on any client decision topic → Condition on the decision.

**What users see:** Nothing new until the reply. Activity shows waiting. Then Rehydrate or the inbound message continues the run.

**Do not use** for “wait two hours then email.” That is **Wait**. Topics must match the Decision reply topics exactly.

### Condition

**Step:** Condition (`condition`)

Checks event data or earlier output. Multiple rules must **all** match. A linear Condition records `context.<step_id>.matched` and continues. Named yes/no paths need branch config (templates / API **Branch**).

**Fields**

* **Rules** — each has a path (`payload.priority`, `context.step_update.updated_count`), operator, and optional value.

Operators: `equals`, `not_equals`, `contains`, `is_empty`, `is_not_empty`, `greater_than`, `less_than`, `before`, `after`.

**Example:** Only continue when `payload.priority` equals `High`.

**What users see:** Nothing. Later steps run or skip.

**Do not use** as a substitute for trigger **Where it happens**. Bind the trigger tightly first.

### Wait

**Step:** Wait (`wait`)

Pauses on a **clock**, saves context (dehydrate), and continues later (rehydrate).

**Fields**

* **Delay seconds** — relative. `3600` is one hour; `86400` is one day.
* **Wait until** — optional absolute ISO time (`2026-07-01T14:00:00.000Z`).
* **Timer escalation step** — optional step if the timer fires before someone clicks Rehydrate.

**Example:** After submit → Wait 86400 → Notify manager if still open.

**What users see:** Nothing during the wait. Activity shows waiting and **Rehydrate**. Builder Rehydrate resumes the **normal** next step (not escalation). Timer expiry uses the escalation step when set.

**Do not use** when the next step must happen in the same tap. Put Show message / Refresh **before** Wait.

### End

**Step:** End (`end`)

Marks the path complete. No fields.

**What users see:** Nothing. The run completes.

**Do not use** as required. The last step with no next step also finishes. End is for a clear canvas and Activity timeline.

***

## Recipe-only steps (not on Add a step)

Wizards such as **Send to Needs Review** insert these. The inspector still edits them like Update record.

### Change status

**Step:** Change status (`change_status`)

Writes a status/select on the current record. Same **Data source**, **Record reference path**, and **Field mappings** as Update record — typically one status field.

When Live screen updates is on, `Change status → Refresh → Show message` updates the open details tab (Approve-style buttons).

**Do not use** if several unrelated fields must change in one step. Use Update record.

### Assign user

**Step:** Assign user (`assign_user`)

Writes a person / assignee field. Same picker shape as Update record.

**Do not use** to send email. Pair with Notify if the assignee should be told.

### Branch

**Step:** Branch (`branch`)

Multi-way split with labeled conditions. The Add a step grid exposes **Condition**; Branch appears on recipe/API routes. If no branch matches, the executor uses `next_step_id`.

***

## What users see

Nothing from Create, Wait, Condition, End, or fire-and-forget Notify except the **result** (a new row, an email, a later toast).

They see Open screen, Show or hide, Set control text, Refresh, Show message, and Stop action **only** when those steps are still in the live path.

When **Live screen updates** is on: Update / Change status / Assign user / Call webhook and wait can Refresh / Open screen / Show message on the tab they already have open.

When it is **off**, Refresh after Update cannot update that tab. Create, Wait, and Notify stay background-only either way. Inbound webhooks into NotionApps cannot refresh an already-open tab.

## Limits and plans

* Messaging steps need Messaging entitlement.
* Create related records needs a real relation property.
* Call webhook and wait needs a reachable HTTPS URL and a JSON Webhook response.
* Update / Change status on the same sheet as a **Record updated** trigger can loop — add a Condition.
* Simulate / Test workflow still create runs unless the tool says otherwise.

## Example

Inspection Assign Job form → `form_submitted` → Create related records (photo slots) → Notify inspector. Put Show message **before** Create if the submitter should see a toast in the same tap.

Approve on an open details tab (Live screen updates on): Change status → Refresh data → Show message.

Wait for Make, then refresh the same tab: Call webhook and wait → Refresh data → Show message.

## Fix problems

| Symptom                                                   | Likely cause                                            | What to do                                                                                                                           |
| --------------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| Submit works but no toast / no refresh                    | Show message or Refresh is after Create / Notify / Wait | Move live-screen steps above background steps, or use Call webhook and wait + Live screen updates                                    |
| Refresh after Update does nothing on the open tab         | Live screen updates is off                              | NotionApps turns the switch on from Workflow Operations → Controls                                                                   |
| Call webhook and wait shows Working… then asks to refresh | Make did not return Webhook response JSON in time       | End the scenario with Webhook response. Do not use Notify → Webhook                                                                  |
| Create/Update validation error                            | Missing data source or field mappings                   | Choose the sheet and at least one mapping                                                                                            |
| Related records create nothing                            | Wrong relation field                                    | Relation must point at the parent record                                                                                             |
| Double runs                                               | Update wrote a sheet that also has Record updated       | Remove one trigger or add a Condition                                                                                                |
| Messaging step does nothing                               | Entitlement or topic mismatch                           | Match Decision reply topics exactly. See [Topics that perform work](https://docs.notionapps.com/automation/topics-that-perform-work) |
| Email `simulated`                                         | Local development                                       | Expected locally. Staging/prod actually send                                                                                         |

## Related

[Trigger catalog](https://docs.notionapps.com/automation/advanced-reference/trigger-catalog). [Start From a Wizard](https://docs.notionapps.com/automation/start-from-a-wizard). [Workflow Foundation User Guide](https://docs.notionapps.com/automation/workflow-foundation-user-guide). [Workflow How-To Guide](https://docs.notionapps.com/automation/notionapps-workflow-how-to-guide). [Webhooks and Integrations](https://docs.notionapps.com/automation/webhooks-and-integrations).
