> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getdialed.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Human in the loop

> Pause a flow until a person answers, correlate their reply back to the exact step that asked, and continue with what they said in context

Some flows have to stop and ask somebody. Approve this refund. Confirm this list is the right one. Send me back the spreadsheet. A **send-and-wait** step does exactly that: it sends the question, parks the run, and continues when the answer arrives — or when the step's own timeout runs out.

Runs are durable, so a step can wait minutes or days without holding anything open and without losing a byte of progress.

## One step, not two

A send-and-wait is **one task**. It is not a send step followed by a separate wait step, and it is not a new execution model — it is the ordinary signal wait the platform already has, with the send composed in front of it and the correlation wiring done for you.

Turn it on with `wait_for_reply` in the task's `execution_config`:

```json theme={null}
{
  "task_id": "task_ask",
  "task_name": "Ask the owner for the list",
  "platform_id": "postmark",
  "service_id": "postmark__email",
  "action_id": "postmark__email__send_and_wait",
  "connection_id": "cred_e5f6g7h8",
  "execution_type": "immediate",
  "execution_config": {
    "wait_for_reply": true,
    "timeout": "24h",
    "on_timeout": "continue",
    "on_timeout_output": { "answered": false }
  },
  "parameters": {
    "from": "requests@example.com",
    "reply_to_base": "requests@reply.example.com",
    "to": ["{{ input.owner_email }}"],
    "subject": "Please send this week's list",
    "text_body": "Reply to this message with the CSV attached."
  }
}
```

That ordering matters and is guaranteed: the platform registers what it is waiting for **before** the message leaves. A person on a fast mail client can answer in under a second, and a reply that arrives before the platform is listening would simply vanish — nothing would error, the flow would just sit until its timeout.

<Note>
  **`wait_for_reply` is honoured only on a step built to wait.** Putting it on an arbitrary action is refused with a `422` naming the field when you save the definition, and refused loudly at run time for definitions saved before that check existed. There is no arm where it is silently ignored — an author who asked for a wait and got a fire-and-forget send would have no indication that the half they cared about never happened.
</Note>

### The v1 boundary

A reply-waiting step must be `immediate`. Combining `wait_for_reply` with `scheduled` or `windowed` is refused with a `422` naming the field.

That is a deliberate boundary rather than an oversight: the correlation record's deadline is derived from the wait's own timeout **at the moment the task runs**, so a task that sat in a window for six hours first would either need its record written before a delay of unknown length, or would silently move its own deadline. Neither is a thing to guess at, and you are owed the answer at save time rather than at 2 a.m. Put the delay in a step *before* the ask instead.

## How a reply finds its way back

Two channels ship, and they correlate a reply differently because the medium gives them different things to work with. [Email](/concepts/transactional-email) carries a token on the message itself; [SMS](/concepts/sms) has nowhere to put one and matches on the **pair of numbers** instead. Everything after correlation — the timeout policies, the resumed payload shape, the parked-run status — is identical on both.

### Email: a correlation token

Each send carries a fresh, unguessable, single-use **correlation token**. It is what tells the platform which run, and which step of it, an incoming answer belongs to — and it is what authorises the resume, so it is treated as a credential: it is generated with cryptographic randomness, only its hash is ever stored, and it never appears in a log line.

The token travels on **two carriers**, so a reply that loses one still matches:

| Carrier                   | What it looks like                        | Why                                                                                                                       |
| ------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| A per-send reply address  | `requests+<token>@reply.example.com`      | The primary. Hitting **Reply** puts it back in the envelope automatically, with no cooperation from the person answering. |
| A tag on the subject line | `Please send this week's list [#<token>]` | The backup. Mail clients preserve bracketed subject decorations verbatim through `Re:` and `Fwd:` prefixes.               |

<Warning>
  **Do not edit the tag on the subject line when you reply, and do not strip the reply address.** Both carry the identifier that matches your answer to the step waiting for it. A reply that carries neither cannot be matched to anything, and the symptom is not an error — the flow simply waits out its timeout as though nobody answered.

  If you are writing the message body yourself, say so to the person you are asking. "Reply to this message" is enough; "start a new message to this address" is not.
</Warning>

You supply the **mailbox** the replies come back to, as `reply_to_base` — a plain address like `requests@reply.example.com`. The platform builds the per-send address from it. Do not supply an address that already contains a `+`.

<Note>
  The reply mailbox needs a domain that actually accepts incoming mail. For the email channel that means an inbound domain bound to the server your credential belongs to, with its `MX` record published — see [receiving replies](/concepts/transactional-email#receiving-replies), and note the warning there about which domain to use. Without it, replies never arrive and every wait ends on its timeout.
</Note>

### SMS replies

A text message has no reply address to decorate and no subject line to tag, so there is nowhere to put a token. An SMS reply is matched instead on **the pair of numbers involved** — the handset the message went to, and the number it went from — recorded before the message leaves and recomputed from the reply when it arrives.

Three consequences follow, and all three are things you can hit on your first attempt:

**The message must come from one specific number you own.** A Messaging Service picks a number from its pool at the moment the message goes out, so a reply would arrive from a number no wait was ever registered against — the flow would sit until its timeout on an answer the person really gave. A reply-waiting step configured with a Messaging Service is refused by name **before anything is sent**. See [both sender forms](/concepts/sms#both-sender-forms).

**One conversation per pair of numbers at a time.** If a wait is already outstanding between those two numbers, a second reply-waiting step between the same pair is **refused** — immediately, by name, and before any message goes out, so it costs no text and no budget. The alternative was letting the newer step take over the pair, which would silently steal the first flow's answer: the first flow has already sent its message and is sitting in its wait, and it would then time out on a reply the human actually sent. A loud refusal for one author beats an undiagnosable silence for two.

If you need to ask the same person two things at once, ask them from two different numbers, or sequence the steps.

**Keep the wait short.** People answer a text in minutes and abandon it in hours, so an email-shaped `timeout: "24h"` mostly buys dead flows — and because the correlation key is the number pair, a long wait is also a long window in which a second flow reaching that person is refused.

<Note>
  **There is no `MX` record and no inbound domain to configure for SMS.** Replies arrive on the number itself. What the platform needs instead is your Twilio account's own auth token, because that is what Twilio signs its inbound webhooks with — see [connecting Twilio](/concepts/sms#connecting-twilio) for the mistake that quietly breaks this.
</Note>

## Who is allowed to answer

Two conditions, and the first is the one doing the real work:

1. **The reply must carry the correlation token.** It is unguessable and single-use — the first valid answer consumes it, so a second copy of the same reply cannot resume the same step twice.
2. **By default, the reply must come from the address the message was sent to.** Defence in depth: an answer that carries a valid token but arrives from somewhere else is not acted on.

**A reply resumes the parked task exactly once.** The token is consumed by the first valid answer, so a second copy of the same reply — a mail server retrying, a person hitting Reply twice, the provider redelivering after a lost acknowledgement — cannot resume the step again. Deliveries are recorded as they are handled, so a redelivery of a reply that was already acted on is recognised and produces no second resume. A redelivery of one that was *not* fully handled still converges on exactly one.

**An answer that matches nothing is accepted and dropped, not reported as an error.** A reply carrying no recognisable token, a token for a wait that has already timed out, or a sender that does not match, is acknowledged to the mail provider and goes no further. It has to be: reporting the difference would let anyone with the reply address probe which tokens are live, and refusing it would make the provider retry a message nothing will ever match. The flow is unaffected — from its point of view nobody answered.

**On SMS the two conditions collapse into one.** The pair of numbers *is* the correlation and the authorisation at the same time: a reply that correlates has, by construction, come from the handset the message was sent to. `sender_matched` is still in the resumed payload, and it still reads `true`.

Some workflows need the second condition relaxed — the person you asked forwards it to a colleague, or a shared mailbox answers on their behalf. Set `sender_match: false` in the step's `execution_config` to accept any sender that carries the token.

```json theme={null}
{
  "execution_config": {
    "wait_for_reply": true,
    "sender_match": false,
    "timeout": "24h"
  }
}
```

Relaxing it does not hide anything: `sender_matched` is in the resumed payload **either way**, so flow logic can always branch on whether the answer came from the person who was asked.

## What lands in context on resume

When the answer arrives, the step's output is a **normalized payload** — the same shape regardless of which channel the reply came back on. Provider-specific data never reaches your flow:

| Field                 | What it holds                                                                                                                                                                                                                                               |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `channel`             | Which pathway the reply came back on: `email` or `sms`.                                                                                                                                                                                                     |
| `from_address`        | The sender, normalized — an email address on `email`, the replying handset's E.164 number on `sms`.                                                                                                                                                         |
| `from_name`           | The sender's display name, when there was one.                                                                                                                                                                                                              |
| `to`                  | Who the reply was addressed to.                                                                                                                                                                                                                             |
| `subject`             | The reply's subject line. Empty on `sms`, which has no such thing.                                                                                                                                                                                          |
| `body`                | The reply's text.                                                                                                                                                                                                                                           |
| `body_truncated`      | `true` when the body was longer than the platform carries and you are seeing a prefix. Branch on it before branching on body content.                                                                                                                       |
| `attachments`         | A **reference** to each attached file — see [attachments](#attachments-arrive-as-file-references). **Always empty on `sms`**: the channel is text-only in both directions, so an inbound picture message is out of scope rather than silently dropped here. |
| `num_segments`        | How many SMS segments the reply arrived in, as the provider counted them. `null` on a channel that has no such concept — which is what email reports, and it means *not applicable*, never zero.                                                            |
| `delivery_failed`     | `true` when this payload is ending the wait because the **outbound** message never reached the handset — see [when the message never arrives](#when-the-message-never-arrives). `false` on every ordinary reply.                                            |
| `failure_reason`      | Why the outbound message never arrived, as a stable named reason. Meaningful only when `delivery_failed` is `true`; `null` otherwise.                                                                                                                       |
| `provider_message_id` | The provider's identifier for the reply.                                                                                                                                                                                                                    |
| `provider_thread_id`  | The provider's identifier for the conversation, when it publishes one.                                                                                                                                                                                      |
| `received_at`         | When the reply arrived.                                                                                                                                                                                                                                     |
| `sender_matched`      | Whether the reply came from the address that was asked.                                                                                                                                                                                                     |

Read them like any other step output:

```text theme={null}
{{ step_ask.output.from_address }}
{{ step_ask.output.body }}
{{ step_ask.output.attachments[0].filename }}
{{ step_ask.output.sender_matched }}
```

Keeping this shape platform-neutral is the point: a flow authored against an email reply keeps working unchanged when the same wait is later answered over another channel.

## Attachments arrive as file references

An answer carrying a file gives you a **reference** to it, never its contents:

| Field          | What it holds                                                                                             |
| -------------- | --------------------------------------------------------------------------------------------------------- |
| `file_id`      | The [stored file](/concepts/files). Every file-taking step accepts it.                                    |
| `filename`     | The file's name, as sent.                                                                                 |
| `content_type` | What kind of file it is said to be.                                                                       |
| `size_bytes`   | How large the stored file is.                                                                             |
| `sha256`       | The checksum, computed here over the bytes as they were stored — our attestation, not the sender's claim. |
| `disposition`  | Whether it was attached or embedded in the message.                                                       |
| `content_id`   | The identifier an embedded file is referenced by from inside the body.                                    |

The attachment's bytes are decoded, hashed and **stored at the moment the reply arrives**, before the flow is woken. So by the time a later step reads the reference, the file already exists — there is nothing to fetch, and no third-party address to re-visit.

Bytes never ride the resume. They would make the payload grow with the file, and a payload that can grow with its attachment is one that eventually fails the very resume it exists to carry. A payload limit here is a hard error, not a slow path.

Read the reference the same way you read any other stored file:

```text theme={null}
{{ step_ask.output.attachments[0].file_id }}
```

* **To turn a CSV into records**, hand that value to `getdialed__records__parse_csv` as its `file_id`. The contents are read inside the step and never travel through the flow.
* **To work on the text itself**, use `getdialed__files__read_text` first, then bind its `text` output. It is capped at 512 KB and **refuses rather than truncates** — silently short text reads as complete everywhere afterwards.

The [send-and-wait guide](/guides/send-and-wait-for-a-reply) walks both shapes end to end.

<Note>
  **A binary attachment is stored, and reading it as text is refused.** A PDF, a spreadsheet in its own format or an image arrives with a `file_id` like anything else — the platform stores whatever was sent. What is refused is *reading it as text*, by the step that does the reading, because a step that decoded binary into characters would hand you a string of replacement characters that looked exactly like a successful read. The reference is still there, so flow logic can see that the file arrived and say something useful about it.
</Note>

<Note>
  **A stored attachment is kept for 30 days.** Inbound attachments are not permanent: they expire on a fixed horizon rather than living until deleted, because they arrive on a path anybody with the reply address can reach and unbounded storage there is not a thing to offer. A flow that needs a copy for longer should write one out with [file steps](/concepts/files) while the reference is still live.
</Note>

## When nobody answers

The wait ends on the step's `timeout`, and `on_timeout` says what happens next. These are the ordinary [signaled-task](/concepts/execution-models#signaled) timeout policies — a send-and-wait inherits them unchanged:

| `on_timeout`         | What happens                                                                                                  |
| -------------------- | ------------------------------------------------------------------------------------------------------------- |
| `continue` (default) | The step resolves with whatever `on_timeout_output` declares, and the flow proceeds. Branch on it downstream. |
| `abort_step`         | The step resolves as an error, subject to its `on_failure` policy.                                            |
| `abort_execution`    | The whole run fails.                                                                                          |

`continue` with a meaningful `on_timeout_output` is usually what you want, because it turns "nobody answered" into a value the rest of the flow can act on:

```json theme={null}
{
  "execution_config": {
    "wait_for_reply": true,
    "timeout": "48h",
    "on_timeout": "continue",
    "on_timeout_output": { "answered": false, "reason": "no reply in 48h" }
  }
}
```

```json theme={null}
{
  "condition": "{{ step_ask.output.answered }} == false",
  "action": "skip_next_step",
  "reason": "Nobody sent the list — nothing to process"
}
```

<Note>
  **A failed send never enters the wait.** If the message could not be sent, the step ends there rather than parking for hours on a reply to something that never went out. That failure mode is otherwise invisible: nothing errors, the flow simply sits.
</Note>

## When the message never arrives

A send can succeed and the message can still never reach the person — a carrier filters it, the handset is unreachable, the sending number is not registered. When the provider reports that, the wait does not sit through it.

A terminal delivery failure **ends the wait immediately**, with `delivery_failed: true` and a named `failure_reason` naming the provider's status and error code. The step is treated as failed rather than answered, subject to its `on_failure` policy — which is the point: without it, a flow would park for its full timeout on somebody who was never texted, and "the handset is unreachable" and "the human declined to answer" would be the same silence.

A *successful* delivery deliberately does **not** end the wait. Delivery is not an answer — unparking on it would end every conversation the instant the handset received the question.

<Warning>
  **On SMS, this does not fire today, and the reason is a genuine collision between two constraints.** Twilio reports delivery outcomes only for messages sent through a **Messaging Service** — a plain phone number has no such setting — and a reply-waiting step [cannot use a Messaging Service](/concepts/sms#both-sender-forms), because the pool would pick a number no wait could be correlated against.

  So a reply-waiting text gets inbound replies but no delivery outcome, and a message the carrier drops ends the wait on its **timeout** rather than with a named failure. Set a `timeout` you are happy to actually wait, and use `on_timeout_output` to say what "no answer" means for your flow — see [when nobody answers](#when-nobody-answers).

  Email is unaffected: it has no equivalent constraint on its sending address.
</Warning>

## Reading a parked run

While a step is waiting, its [execution](/concepts/batches-and-executions#execution-status-lifecycle) reports the status **`waiting`** — a non-terminal state meaning *parked on an answer, not stuck and not finished*. It returns to `running` the moment the wait ends, whichever way it ends.

Watching the [stream](/concepts/execution-streaming) shows the same thing as it happens: an `execution.waiting` frame when the step parks and an `execution.resumed` frame when it continues. Neither is terminal, so a park never closes an open stream — a detail view can sit on a run that is waiting for a day and pick the story back up when somebody answers.

## Next steps

<CardGroup cols={2}>
  <Card title="Send and wait for a reply" icon="reply" href="/guides/send-and-wait-for-a-reply">
    The end-to-end walkthrough: ask for a CSV, wait, parse it, fan out.
  </Card>

  <Card title="Transactional email" icon="envelope" href="/concepts/transactional-email">
    One of the two channels — sending domains, suppression, and what a send reports.
  </Card>

  <Card title="SMS" icon="comment-sms" href="/concepts/sms">
    The other — both sender forms, strict E.164 recipients, and why acceptance is not delivery.
  </Card>

  <Card title="Execution models" icon="clock" href="/concepts/execution-models">
    The signaled wait a send-and-wait is built on, and its timeout policies.
  </Card>

  <Card title="Execution streaming" icon="signal-stream" href="/concepts/execution-streaming">
    Watch a run park and resume live, without polling.
  </Card>
</CardGroup>
