# Save a flow

Saves a flow, or updates it: the same `key` again changes what you send and keeps what you leave out, and `steps` replaces all steps. To start from another flow, send `based_on` with its key and no steps. Ready-made flows cannot be changed.

**The steps.** `start` is the main line: it runs for everyone, in order, each step its `wait_days` after the step before it (the first at 0). It takes any channel in any order: `email` (up to 5, which you send yourself), `human_task` (a call or another task by a person, up to 3), `linkedin_like` (up to 2), `linkedin_message` (up to 4; only to people who already are a LinkedIn connection, anyone else skips that step and the sequence goes on), `inmail` (up to 2; to anyone who is not a connection, while a connection gets the text as a normal message; it needs LinkedIn Premium, Sales Navigator or Recruiter on the account that sends, otherwise the sequence stops at that step with the reason) and at most one `linkedin_invite` (`note` true or false), as the last step of `start`. Two steps of the same channel there (two emails, two messages, two InMails) are at least 1 day apart. **Without an invite** there are no branches: everyone gets the steps of `start` in order, for example an email, a call and an email. **With an invite** come its branches: up to 4 `linkedin_message` in `after_accept` (the first `wait_days` after the invite is accepted, 0 = a few minutes; each next one at least 1 day after the previous), up to 4 in `if_connected` (for people who already are a connection), up to 2 `inmail` in `not_accepted` for people who did not accept (the first at least 1 day after the invite, the next at least 1 day after the previous InMail; a text and a subject per person), and exactly one `withdraw_invite` as the last step of `not_accepted` (7 to 60 days after the invite, or after the step before it). Up to 3 `email` steps go in each branch after it (`after_accept`, `if_connected`, `not_accepted`), between the other steps of that branch: such an email follows its branch, so it is only due once it is clear whether the invite was accepted (or the person already was a connection). **Without LinkedIn**: a flow of only `email` and `human_task` steps needs no LinkedIn account, no LinkedIn profile of the person (give their `email`) and no month of LinkedIn outreach. Profile visits are not available in a flow yet.

**How waits count.** The first step goes at the start of the sequence (0); to start later, give `start_at` when you start, and for someone you already emailed outside the flow, `sent_steps` on POST /outreach/flows/assign. Each next step of `start` waits its `wait_days` after the step before it, whatever its channel (a call 2 days after an email: `wait_days` 2 on the call). In a branch every step waits its `wait_days` after the step before it in that branch: the first after the invite is accepted (`after_accept`), after the moment of the invite (`if_connected`), or after the invite was sent (`not_accepted`).

**A change.** People started from then on get the new steps; sequences that already run keep theirs. To stop those, pause their campaign (PATCH /outreach/linkedin/campaigns/{id}). The reply says how many campaigns follow the flow.

A flow is the skeleton of a sequence without its texts: which steps, on which channel, in which order and branch, after how many days. Any channel in any order: emails you send yourself, a call or another human task, and LinkedIn steps, with or without a LinkedIn invite, for example an email, a call and an email, or a call, a LinkedIn message and an email. A flow with nothing on LinkedIn (only emails and human tasks) needs no LinkedIn account, no LinkedIn profile of the person (an email address does) and no month of LinkedIn outreach (`uses_linkedin`). You write the texts per person when you start (POST /outreach/linkedin/sequences with `flow` and `texts`, or `start_flow` on the assistant server), and the sequence then has exactly the steps of the flow. There are ready-made flows (`invite_note_then_message`, `invite_then_message`, `invite_note_two_follow_ups`, `invites_only`, `invite_then_inmail`, `like_then_invite`, `email_then_invite`, `email_then_call`, `emails_only`) and the workspace’s own, at most 100. These routes are the tools `list_flows`, `get_flow`, `save_flow`, `delete_flow` and `assign_flow` of the assistant server. Free.

## Authentication

Send `Authorization: Bearer gtk_…`. Create the key under **API keys in the menu**. Every `gtk_` key can call these routes, also while the Signals API is closed. A missing, invalid or revoked key is **401**.

### Body parameters

- **`key`** `string` · **required** — Your own name for the flow: lowercase letters, digits and `_`, starting with a letter, up to 40 characters (`CFOs after funding` becomes `cfos_after_funding`). Use the same key every time.
- **`name`** `string` — How the user sees it, up to 60 characters. Required the first time.
- **`description`** `string | null` — When to use it, up to 300 characters. `null` clears it.
- **`based_on`** `string` — A new flow: copy the steps of this flow (a ready-made one or the workspace’s own) instead of sending `steps`.
- **`steps`** `object[]` — The steps (see above); replaces all steps. Required the first time unless `based_on` is given.
- **`steps[].key`** `string` · **required** — Your name for the step, used for its text (`texts.message_1`): lowercase letters, digits and `_`, up to 30 characters, unique in the flow.
- **`steps[].type`** `string` · **required** — `email` (one you send yourself, in `start` (up to 5) or in a branch after the invite (up to 3 per branch)), `linkedin_invite` (at most one, the last step of `start`), `linkedin_message` (in `start` only to existing connections; or in `after_accept` and `if_connected`), `inmail` (in `start`, or for people who did not accept, before the withdraw), `withdraw_invite` (with an invite: the last step of `not_accepted`), `human_task` (a person on the team calls, records a voice note, follows up, reviews or researches; up to 3 per branch; the sequence waits until the task has an outcome) or `linkedin_like` (mySDR likes the lead's latest post from the last 30 days, without a text; skipped if there is none; in any branch, up to 2 per branch; counts against Max Post Likes/day). One of: `email`, `linkedin_invite`, `linkedin_message`, `inmail`, `withdraw_invite`, `human_task`, `linkedin_like`.
- **`steps[].branch`** `string` — `start`, `after_accept`, `if_connected`, `not_accepted`, or `on_reply` (only `human_task` steps, each with a `task.condition`: they come after a positive or negative reply instead of in the sequence). Without `branch`: without an invite every step goes in `start`; with one, an email, task or like listed before the invite goes in `start`, and anything else by its type (a message or task in `after_accept`, an InMail or the withdraw in `not_accepted`). A LinkedIn message or InMail in `start` before an invite needs `branch: "start"`. One of: `start`, `after_accept`, `if_connected`, `not_accepted`, `on_reply`.
- **`steps[].task`** `object` — Only on a `human_task`, required there: `kind` (`call`, `voice_note`, `follow_up`, `review`, `research`, `custom`), `instruction` (what to do, e.g. a call script; optional: without it the step gets the standard text and rules of its place in the flow, as a new step on the Flows page does), `deadline_days` (overdue this many days after it comes due), `owner` (the id of a member, GET /team; default whoever starts the sequence), `condition` (`positive` or `negative`, only in `on_reply`), `if_no_phone` (a `call` without a known number: `research`, the default, turns it into a research task; `skip` skips the step; `find_phone` waits for a number: POST /person/phone at the normal price, and the task shows it once found), `retry_after_days` (after `no_answer`, default 2), `max_attempts` (default 3; then the flow continues), `on_positive` and `on_negative` (`stop`, the default, or `continue`).
- **`steps[].note`** `boolean` — On the invite: with a note you write per person (`true`) or without (`false`, the default).
- **`make_default`** `boolean` — True: after saving, this flow becomes the default of the workspace (as Set as default on the Flows page). The first own flow already is.
- **`steps[].wait_days`** `integer` — Days to wait (see How waits count above). The first step of `start` goes at the start (0); each next step of `start` waits this many days after the step before it, and two steps of the same channel there are at least 1 day apart; in a branch, each step counts from the step before it in that branch, at least 1 day after the first (in `not_accepted` also the first: at least 1 day after the invite).

## Response

- **`object`** `string` · **required** — Always `flow`. One of: `flow`.
- **`key`** `string` · **required** — The key.
- **`name / description`** `string / string | null` · **required** — How the user sees it.
- **`preset`** `boolean` · **required** — A ready-made flow of mySDR: it cannot be changed or deleted; start from it with `based_on`.
- **`based_on`** `string | null` · **required** — The flow it started from.
- **`archived`** `boolean` · **required** — The user archived it in mySDR (under Flows): it no longer shows among their active flows.
- **`reply_handling`** `object` · **required** — What happens after a reply, besides the human tasks in `on_reply`; only the user sets it, under Reply handling on the Flows page: `notify_positive` (a positive reply posts to their Slack or Teams) and `agent_closes_negative` (you close a negative reply yourself: GET /outreach/replies then says `agent_closes`).
- **`approval_required`** `boolean` · **required** — Whether the texts prepared for a lead in this flow must be approved before they start (the default; only the user sets it, under Approval on the Flows page). If so, a start skips anyone whose prepared texts are not approved yet (`texts_not_approved`), unless the call has `texts_approved` after the user said yes; `flow_texts_to_approve` on GET /tables/leads lists them.
- **`steps`** `object[]` · **required** — `key` (the name of its text in `texts`), `type` (`email`, `linkedin_invite`, `linkedin_message`, `inmail`, `withdraw_invite`, `human_task`, `linkedin_like`), `branch` (`start`, `after_accept`, `if_connected`, `not_accepted`, `on_reply`), `note` (on the invite), `wait_days`, `text` (whether you write a text for it), `sent_by` (`agent` for an email you send yourself, `human` for a human task, otherwise `mysdr`), `task` on a human task, and `connections_only: true` on a LinkedIn message in `start` (it only goes to existing connections).
- **`channels`** `string[]` · **required** — The channels of the flow in the order they first come: `email`, `call` (a human task) and `linkedin`.
- **`uses_linkedin`** `boolean` · **required** — The flow does something on LinkedIn (an invite, a message, an InMail or a like). False: it needs no LinkedIn account, no LinkedIn profile of the person (an email address does) and no month of LinkedIn outreach.
- **`has_invite`** `boolean` · **required** — The flow has a LinkedIn invite, and so branches after it. False: everyone gets the steps of `start` in order.
- **`human_task_steps`** `string[]` · **required** — The human tasks of the flow: the sequence waits on their outcome (POST /human-tasks/{id}/complete); those in `on_reply` come after a reply.
- **`text_steps`** `string[]` · **required** — The step keys that need a text when you start.
- **`email_steps`** `string[]` · **required** — The emails you send yourself, in `start` or in a branch after the invite: their text and subject may come at the start or when the step is due.
- **`inmail_steps`** `string[]` · **required** — The InMails mySDR sends, in `start` (to anyone who is not a connection) or to people who did not accept: each needs a text (`texts`) and a subject (`subjects`) at the start.
- **`ready_needs`** `object` · **required** — What Ready for campaign asks you to prepare with POST /outreach/flows/assign: `texts` (a text per step key) and `subjects` (a subject per email and InMail step). An email step in a lead's `sent_steps` needs nothing.
- **`in_words`** `string[]` · **required** — The steps in plain sentences, to show the user.
- **`created_at / updated_at / via`** `string | null` · **required** — When it was saved, and by which key (`Claude`); null for a ready-made one.
- **`is_default`** `boolean` — The default flow of the workspace: every lead without a flow of its own follows it. On GET, and after a save with `make_default`.
- **`dashboard_url`** `string` · **required** — The flow in mySDR.
- **`created`** `boolean` · **required** — True when this call created it.
- **`campaigns`** `integer` · **required** — How many running or paused campaigns follow this flow.
- **`note`** `string` · **required** — What the save means for people who already run.

## Credits

Free. This call never uses credits.

## Errors

- **400** `invalid_request` — No `key`, a key that is not valid or of a ready-made flow, no `name` or `steps` the first time, steps that do not fit (`param` names the step), or an unknown field.
- **404** `not_found` — `based_on` is not a flow of this workspace.
- **409** `invalid_state` — The workspace has 100 flows already: delete one first.
- **503** `service_unavailable` — Saving flows is not available in this environment yet.
- **409** `invalid_state` — The key has no user behind it, so mySDR cannot record who did it.
- **401** `authentication_error` — The Authorization header is missing, or the key is unknown or revoked.
- **403** `permission_error` — API access is not part of your plan, or your account is suspended.
- **429** `rate_limit_error` — More than 60 requests per minute, or more than 3 at the same time, on one key. Wait for the Retry-After header.
- **503** `service_unavailable` — The API is temporarily unavailable. Retry later.
- **500** `internal_error` — Something went wrong on our side.

## Example

### cURL

```bash
curl -X POST "https://mysdr.app/api/v1/outreach/flows" \
  -H "Authorization: Bearer $MYSDR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "key": "cfo_after_funding",
    "name": "CFOs after funding",
    "description": "For CFOs at companies that just raised.",
    "based_on": "invite_note_two_follow_ups"
  }'
```

### JavaScript

```javascript
const response = await fetch('https://mysdr.app/api/v1/outreach/flows', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.MYSDR_API_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    "key": "cfo_after_funding",
    "name": "CFOs after funding",
    "description": "For CFOs at companies that just raised.",
    "based_on": "invite_note_two_follow_ups"
  })
});
const body = await response.json();
```

### Python

```python
import json
import os
import urllib.request

payload = {
  "key": "cfo_after_funding",
  "name": "CFOs after funding",
  "description": "For CFOs at companies that just raised.",
  "based_on": "invite_note_two_follow_ups"
}

request = urllib.request.Request(
    "https://mysdr.app/api/v1/outreach/flows",
    headers={
    "Authorization": f"Bearer {os.environ['MYSDR_API_KEY']}",
    "Content-Type": "application/json"
    },
    data=json.dumps(payload).encode(),
    method="POST",
)
with urllib.request.urlopen(request) as response:
    body = json.load(response)
```

### Example response

```json
{
  "object": "flow",
  "key": "cfo_after_funding",
  "name": "CFOs after funding",
  "description": "For CFOs at companies that just raised.",
  "preset": false,
  "based_on": "invite_note_two_follow_ups",
  "steps": [
    {
      "key": "invite",
      "type": "linkedin_invite",
      "branch": "start",
      "note": true,
      "wait_days": 0,
      "text": true,
      "subject": false,
      "sent_by": "mysdr"
    },
    {
      "key": "message_1",
      "type": "linkedin_message",
      "branch": "after_accept",
      "wait_days": 0,
      "text": true,
      "subject": false,
      "sent_by": "mysdr"
    },
    {
      "key": "follow_up_1",
      "type": "linkedin_message",
      "branch": "after_accept",
      "wait_days": 3,
      "text": true,
      "subject": false,
      "sent_by": "mysdr"
    },
    {
      "key": "connected_1",
      "type": "linkedin_message",
      "branch": "if_connected",
      "wait_days": 0,
      "text": true,
      "subject": false,
      "sent_by": "mysdr"
    },
    {
      "key": "withdraw",
      "type": "withdraw_invite",
      "branch": "not_accepted",
      "wait_days": 16,
      "text": false,
      "subject": false,
      "sent_by": "mysdr"
    }
  ],
  "channels": [
    "linkedin"
  ],
  "uses_linkedin": true,
  "has_invite": true,
  "text_steps": [
    "invite",
    "message_1",
    "follow_up_1",
    "connected_1"
  ],
  "email_steps": [],
  "inmail_steps": [],
  "human_task_steps": [],
  "ready_needs": {
    "texts": [
      "invite",
      "message_1",
      "follow_up_1",
      "connected_1"
    ],
    "subjects": []
  },
  "in_words": [
    "invite: an invite with a note.",
    "message_1: a message right after the invite is accepted.",
    "follow_up_1: a message 3 days after the previous step.",
    "connected_1: for an existing connection, a message at the start.",
    "withdraw: not accepted after 16 days, the invite is withdrawn and the sequence ends.",
    "An email in a branch follows that branch: it only goes out once it is clear whether the invite was accepted.",
    "A reply stops the sequence for that person."
  ],
  "created_at": "2026-10-28T08:00:00.000Z",
  "updated_at": "2026-10-28T08:00:00.000Z",
  "via": "Claude",
  "archived": false,
  "reply_handling": {
    "notify_positive": false,
    "agent_closes_negative": false
  },
  "approval_required": true,
  "dashboard_url": "https://mysdr.app/en/gtm/flows?flow=cfo_after_funding",
  "created": true,
  "campaigns": 0,
  "note": "Saved. Start people with it: start_flow (POST /outreach/linkedin/sequences) with flow and the texts per step key (texts)."
}
```
