---
title: "Create Step"
method: POST
path: "/workflows/{workflow_id}/steps"
tags: ["Workflows::Step"]
---

# Create Step

`POST /workflows/{workflow_id}/steps`

Add a new step to a workflow. `step_type_settings` must contain exactly one key
naming the step type.

**Placement:** omit `parent_step_id` to append to the trunk sequence. Set
`parent_step_id` + `branch` to place inside a split branch. Use `position` (0-based)
or `after_step_id` to control order within a container.

For `split_test_step`: provide `variants` with exactly 2 entries whose `weight`
values sum to 100. For `conditional_split_step`: optionally provide
`condition.filter_id` (integer id or obfuscated RefineFilter public_id).

## Path parameters

- `workflow_id` string, required

## Request body

- WorkflowsStepParametersCreate — Parameters for adding a new step to a workflow. `step_type_settings` must contain exactly one key naming the step type. ### Placement - **Trunk (default)**: omit `parent_step_id` and `branch` to append at the end of the main sequence. - **Inside a split branch**: set `parent_step_id` to the id (integer or public_id) of the split step, and `branch` to: - `matched` or `unmatched` for a `conditional_split_step` (the legacy `match`/`fallback` aliases are still accepted) - A 0-based variant index string (`"0"`, `"1"`) for a `split_test_step` - **Specific position**: use `position` (0-based integer) or `after_step_id` (id of the preceding step). For `split_test_step`, the `variants` array must have exactly 2 entries with integer weights summing to 100. For `conditional_split_step`, provide `condition.filter_id` (integer id or obfuscated RefineFilter public_id).
  - `workflows_step` object, required
    - `name` string — Optional human-readable name. Defaults to a titleized version of the step type.
    - `parent_step_id` union — Integer database id or obfuscated public_id.
      - integer
      - string
    - `branch` string — Branch selector — `matched`/`unmatched` for conditional splits (legacy `match`/`fallback` still accepted); `"0"`/`"1"` for split tests.
    - `position` integer — 0-based position within the container. Mutually exclusive with `after_step_id`.
    - `after_step_id` union — Integer database id or obfuscated public_id.
      - integer
      - string
    - `step_type_settings` object, required — Exactly one key naming the step type, with its settings as the value. On a `send_email_step` a `template_id` is copied into a new step-owned template, and you may instead pass the write-only `html_body`/`text_body` to author one inline. **See the `step_type_settings` field on the Workflows::Step response schema (`#/components/schemas/WorkflowsStepAttributes`) for the full per-type field catalog, the `send_email_step` inline-body rules, and FK id handling.**

## Response `201`

Created

- WorkflowsStepAttributes — A single action or split step within a workflow. `step_type` equals the single key inside `step_type_settings` and identifies which step class this is. The full list of step types, the per-type settings each one carries, the write-only `send_email_step` inline-body fields, and how FK ids are serialized are all documented on the **`step_type_settings`** field below — that field is the single canonical reference, and the Create Step / Update Step request bodies point to it. `parent_step_id` and `branch` report where a step sits when it lives inside a split branch; see the Create Step / Update Step schemas for setting placement on write.
  - `id` integer — Step ID
  - `public_id` string, nullable — Obfuscated step ID
  - `workflow_id` integer — Integer ID of the parent workflow
  - `step_type` string — The step class key (same as the single key in `step_type_settings`).
  - `name` string — Human-readable step name.
  - `sort_order` integer — Zero-based position **within this step's own container** (the trunk, or a split's branch/convergence sequence). It is therefore **container-local, not globally unique** — it restarts at 0 for steps after a split. In `/structure`, follow the `steps` array order for execution flow, not `sort_order`.
  - `state` 'active' | 'notSetup' | 'notDefined' — Setup state of the step.
  - `parent_step_id` integer, nullable — Raw integer id of the parent split step when this step lives inside a branch. Null for trunk steps.
  - `branch` string, nullable — For conditional splits — `matched` or `unmatched`. For split tests — the 0-based variant index as a string. Null for trunk steps.
  - `convergence_step_id` integer, nullable — Id of the step a split's branches reconverge at — the first step that runs after the split, which every branch continues to. Example: a conditional_split_step (id 203) whose matched/unmatched branches both lead to the same next step (id 206) → convergence_step_id 206. It is null in two cases: a split that ends the flow (nothing follows it), and any non-split step (the field only applies to conditional_split_step / split_test_step). Use step_type to tell them apart — a null on a non-split step just means the field does not apply. When non-null, the referenced step always appears in this response.
  - `step_type_settings` object — A single-key map: the one key is the step type (identical to `step_type`) and its value carries that type's settings. **This is the canonical reference for step settings — the Create Step and Update Step request bodies point here.** Available types — **Action steps** (non-branching): `send_email_step`, `send_asset_step`, `send_chat_message_step`, `contact_tag_step`, `contact_custom_attribute_step`, `delay_step`, `notify_step`, `deliver_webhook_step`, `enroll_step`, `unenroll_contact_step`, `grant_community_access_step`, `revoke_community_access_step`, `create_opportunity_step`, `move_step` **Split steps** (branching): `conditional_split_step`, `split_test_step` ### Per-type fields - `send_email_step`: `subject`, `preheadline`, `template_id`, `from_address_id`, `reply_to_address_id`, plus write-only `html_body`/`text_body` (see "send_email_step inline body" below) - `send_asset_step`: `asset_id` - `send_chat_message_step`: `inbox_key`, `message`, `subject`, `start_new_conversation` - `contact_tag_step`: `action` (`"Add Tags"` or `"Remove Tags"`), `contacts_tag_ids` (array of raw integer tag ids) - `contact_custom_attribute_step`: `action`, `value`, `field_id` (a Forms::Field id — discoverable via the Forms API) - `delay_step`: `delay_type` (`relative`/`day_of_week`/`day_of_month`), `duration`, `interval`, `time_zone`, `use_contact_time_zone`, `use_execution_window`, `day_of_week`, `date_of_month`, `start_time`, `end_time`, `execution_window_days` - `notify_step`: `message`, `notify_type` (array), `user_ids` (array of user ids — must be team members; discover via `GET /teams/{team_id}/memberships` → `user_id`. A non-member id is rejected 422.) - `deliver_webhook_step`: `webhooks_outgoing_endpoint_id` - `enroll_step`: `course_ids` (array), `section_ids` (array). **Enrollment happens per `section_ids`** — provide the section ids to enroll in (discover via `GET /api/v2/courses/{course_id}/sections`). `course_ids` alone is accepted and the step reads `active`, but enrolls nothing at run time. - `unenroll_contact_step`: `course_ids` (array), `section_ids` (array). Unenroll honors both `course_ids` and `section_ids`. - `grant_community_access_step`: `space_ids` (array) - `revoke_community_access_step`: `space_ids` (array) - `create_opportunity_step`: `name`, `value`, `assignment_strategy`, `move_existing`, `sales_pipelines_stage_id`, `sales_pipeline_id`, `membership_id`, `membership_ids` (array) - `move_step`: `workflow_id` - `conditional_split_step`: `condition` object with `filter_id` (raw integer RefineFilter id on read; integer id or public_id on write) and `is_setup` boolean - `split_test_step`: `variants` array, each with `weight` (0..100, two entries summing to 100) ### `send_email_step` — template & inline body A `send_email_step` always owns its email. On **create**, a provided `template_id` is **copied** into a fresh step-owned template (it is never a live reference to the original), and an inline `html_body`/`text_body` instead mints a new step-owned template. This deliberately mirrors the builder UI, which always makes its own copy so edits to a step's email never touch the source. `template_id` takes precedence when both a `template_id` and an inline body are supplied. Because the step owns its template, `template_id` is **create-only and cannot be changed on update** — a `PATCH` that includes `template_id` returns `422`. To change an existing step's email, edit it **in place** by sending `html_body`/`text_body` (this patches the step's own template — no new template is created); to use a different template, create a new step. `html_body`/`text_body` are write-only and are never returned in `step_type_settings`. Prefer a verified sending address for `from_address_id`/`reply_to_address_id`. ### FK id serialization Response FK values (`*_id`, `*_ids`) are serialized as **raw integers**. On write, those same FK fields accept either integer ids or obfuscated public_ids in the same workspace/team scope.
  - `created_at` string, date-time
  - `updated_at` string, date-time

## Other responses

- `401` — Unauthorized
- `403` — Forbidden
- `404` — Not Found
- `422` — Unprocessable Entity

---

[API](https://skmtc.net/myclickfunnels/apis/clickfunnels-api.md) · [All operations](https://skmtc.net/myclickfunnels/apis/clickfunnels-api/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/myclickfunnels/clickfunnels-api/revisions/ae6313eaa176/schema)
