# Questions to User

> POST …/dialogs and sparku ask — options, confirmation, timeout, attachment; how an agent asks for permission.

## How it works

`POST /v1/integrations/{slug}/dialogs` shows a question on the island and keeps the
connection open until the user answers (or the question is withdrawn). The answer comes in the
body of the same HTTP response — `curl` or an HTTP client that doesn't close the
connection early needs to wait for `timeout` from the request body.

An integration can have at most one unanswered question at a time: a second `dialogs`
before an answer to the first gets `409 dialog_pending`. Dialogs must be explicitly
enabled in the integration settings.

## Request body

A question with options — has `options` or `custom`:

```json
{
  "message": "Deploy to prod?",
  "title": "Deploy",
  "options": ["yes", {"id": "no", "label": "Cancel", "detail": "do nothing"}],
  "multiple": false,
  "custom": false,
  "timeout": "10m"
}
```

Confirmation — no `options` or `custom`:

```json
{"message": "Delete branch feature/x?", "accept": "Delete", "decline": "Keep", "comment": true}
```

| Field | Type | Default | Rule |
|---|---|---|---|
| `message` | string | — | required, up to 600 characters |
| `title` | string | integration address | up to 60 characters |
| `options` | array of strings or `{id, label, detail?}` | — | up to 9 options; string is both id and label |
| `multiple` | bool | `false` | select multiple — requires ≥ 2 options |
| `custom` | bool | `false` | add free-text answer field |
| `accept` / `decline` | string | "Yes" / "No" | confirmation button labels, up to 30 characters |
| `comment` | bool | `false` | allow comment on decline |
| `timeout` | number of seconds or `"90s"` / `"10m"` / `"2h"` | `600` (10 min) | 5 seconds to 1 day |
| `attachment` | absolute path | — | file the user can open from the question; must exist |

With `options` or `custom` — a question with options; otherwise — a confirmation with two
buttons. You can't specify both `options`/`custom` and `accept`/`decline` — `accept`/`decline` apply only to confirmation.

## Answer

Always `200 OK`; the response body depends on the outcome:

| Situation | Body |
|---|---|
| selected option(s) | `{"answer": "chose", "options": ["yes"]}` |
| custom answer | `{"answer": "custom", "text": "…"}` |
| accepted | `{"answer": "accepted"}` |
| declined | `{"answer": "declined"}` or `{"answer": "declined", "comment": "…"}` |
| timeout expired | `{"answer": "expired"}` |
| question withdrawn | `{"answer": "withdrawn"}` |

`withdrawn` is returned if: the client disconnects before answering (though then there's
no one to answer — the question is just removed from the island), SparkU closes, the API is disabled,
or the integration is disabled/deleted while a question is waiting for an answer.

To hide a question with the "Hide" button only works for an integration with a card — you can
bring it back with the "Reply" button in the card; until then it remains unanswered on the server.

## Example: curl

```bash
curl --unix-socket ~/.sparku/run/api.sock \
  -H "Authorization: Bearer $SPARKU_TOKEN_DEPLOY" \
  -d '{"message": "Deploy to prod?", "accept": "Yes", "decline": "No"}' \
  http://sparku/v1/integrations/deploy/dialogs
```

```json
{"answer": "accepted"}
```

## `sparku ask`

```
sparku ask <slug> "question" [-o id:label …] [--multi] [--custom]
           [--accept Yes --decline No --comment] [--title …] [--timeout 10m] [--attach file]
```

```bash
sparku ask deploy "Deploy to prod?" -o yes:Yes -o no:No
sparku ask deploy "Where to deploy?" -o prod:Prod -o staging:Staging --custom
sparku ask deploy "Delete branch?" --accept Delete --decline Keep --comment
```

Selected options are printed to stdout one per line (id, not label), custom answer as text, and
the decline comment also to stdout.

### Exit codes for `ask`

Separate from `<sysexits.h>` used by `push`/`end`/`doctor` — so `if sparku ask deploy "Deploy?"; then …` works:

| Code | Meaning |
|---|---|
| `0` | answer received (choice or custom text — in stdout) |
| `1` | declined |
| `2` | timeout expired, question withdrawn, or "Hide", or SparkU closed |

Transport errors (no socket, invalid token, `409 dialog_pending`) return regular `sparku` codes —
`69`, `77`, `69` respectively, see [sparku CLI](/docs/api/cli.md#exit-codes).

```bash
if sparku ask deploy "Deploy to prod?"; then
  ./deploy.sh
else
  echo "deploy cancelled"
fi
```

## For AI agents

An agent like Claude Code or Codex that runs commands in a shell can ask for permission the same
way a CI script does: through `sparku ask` to an integration the user set up beforehand (for example,
"Agent" with the slug `agent`; if it has a card, the question can be hidden and answered later). This is not a separate protocol — an agent is just another API client,
like any other process.

```bash
if sparku ask agent "Delete branch feature/old and close its PR on GitHub?" \
     --accept Delete --decline "Don't touch" --timeout 90s; then
  git push origin --delete feature/old
  gh pr close 42
else
  echo "cancelled by user" >&2
  exit 1
fi
```

While a question is on the island, the agent blocks on the `sparku ask` call — this is expected:
the user makes the decision, and the dangerous step can't run until they answer.
If the user doesn't answer before `--timeout`, default 10 minutes, `ask` returns `2`, and the
agent should treat it as a refusal, not as permission to proceed.

> [!TIP]
> The tool an agent runs commands with usually has its own timeout (2 minutes by default in
> Claude Code). Keep `--timeout` below it, otherwise the agent stops waiting before the user
> can answer, and the question is withdrawn from the island.

An instruction for the agent (in `CLAUDE.md`, `AGENTS.md`, etc.) can read:

```text
Before irreversible actions (deleting, force-pushing, deploying, migrations), ask
the user: sparku ask agent "<what and why>" --accept Yes --decline No --timeout 90s
Exit 0 — go ahead, 1 — declined, 2 — no answer: treat it as declined.
```
