# Add write-back to a Dive
> Add buttons and forms to a Dive that run a Flight, so viewers can write data back, refresh a pipeline, or act on a row, and see the result when the run finishes.
**Write-back** lets viewers act on a Dive, not only read it. A Dive writes back by running a [Flight](/key-tasks/flights/): add a button or a form that starts a run, and viewers can approve an order, refresh a pipeline, or submit a correction without leaving the Dive. The Flight can also call other services, such as posting to Slack or opening a ticket. When the run finishes, the Dive shows the new data.

The Dive itself stays read-only. The Flight does the writing, as its owner, with the owner's token and secrets. The Dive starts the run, follows its status, and reruns its queries when the run succeeds:

```mermaid
flowchart LR
    A["Viewer clicks<br/>a button"]:::green --> B["useRunFlight<br/>starts a run"]
    B --> C["Flight runs as its owner<br/>and writes data"]:::yellow
    C --> D["useFlightRun reports<br/>SUCCEEDED"]
    D --> E["Dive refetches<br/>its queries"]:::green
```

Here, a viewer approves order #1042 in an **Order approvals** Dive, which runs the `decide_order` Flight built in this guide:

![A viewer clicks Approve on order 1042 in an Order approvals Dive. A banner shows the run queued and updating, then Run #4 succeeded. Orders updated, and the order's status changes from pending to approved](img/writable-dives-approve-order.gif)

What happens in the recording:

1. **The viewer clicks Approve on #1042.** The Dive calls `run({ ORDER_ID: "1042", DECISION: "approved" })` from `useRunFlight`, which calls `MD_RUN_FLIGHT` with those values as this run's config. The banner reads **Starting…**, and every button is disabled so the viewer can't start a second run.
2. **The run is queued.** `useRunFlight` returns the new run's number, and `useFlightRun` starts checking its status. While the status is `PENDING`, the banner reads **Run #4: Queued…**.
3. **The Flight runs.** The status moves to `RUNNING`, and the banner reads **Updating order…**. The Flight runs as its owner, reads `ORDER_ID` and `DECISION` from its environment, and updates the row in `ops.main.orders`.
4. **The run succeeds.** `useFlightRun` reports `SUCCEEDED`, and its `onFinished` callback runs the orders query again. The banner reads **Run #4 succeeded. Orders updated.**, and #1042 moves out of the pending list as approved.

The Dive only reads data. The write happens in the Flight, and the Dive learns about it by following the run and querying again.

## What you can build

| Pattern | What the viewer does | What the Flight does |
|---|---|---|
| **Write-back form** | Fills in fields and clicks **Submit**. | Inserts or updates rows with the submitted values, for example a forecast override or a data correction. |
| **Row action** | Clicks **Approve** on a row. | Acts on that record, for example approves an order, closes a ticket, or reprocesses one file. |
| **Refresh button** | Clicks **Refresh data**. | Reruns the pipeline that feeds the Dive, for example pulling the latest CRM records. |
| **Background task** | Clicks **Send report**. | Does work outside MotherDuck, for example calls an API, sends an email, or posts to Slack. |

## Prerequisites

- A Flight you own that does the work. See [Running Python with Flights](/key-tasks/flights/).
- A Dive you can edit. See [Creating visualizations with Dives](/key-tasks/dives/).

## Step 1: Write a Flight that takes input

A Dive passes input to a Flight as [config values](/key-tasks/flights/flights-authentication-config-and-secrets), which the Flight reads as environment variables. Define every key the Dive will send when you create the Flight, because a run can only override keys the Flight already has.

This Flight approves or rejects one order:

```sql
SELECT flight_id
FROM MD_CREATE_FLIGHT(
    name := 'decide_order',
    config := MAP {'ORDER_ID': '', 'DECISION': ''},
    requirements_txt := 'duckdb==1.5.3',
    source_code := $$
import os
import duckdb

def main():
    order_id = os.environ["ORDER_ID"]
    decision = os.environ["DECISION"]
    if decision not in ("approved", "rejected"):
        raise SystemExit(f"unknown decision: {decision!r}")

    con = duckdb.connect("md:")
    con.execute(
        "UPDATE ops.main.orders SET status = ?, decided_at = now() WHERE order_id = ?",
        [decision, order_id]
    )
    print(f"order {order_id}: {decision}")

if __name__ == "__main__":
    main()
$$
);
```

:::warning
Treat config values as input from whoever clicks the button. Validate them, and pass them to SQL as parameters, as above, rather than formatting them into the query text.
:::

Note the `flight_id` the call returns. The Dive needs it in Step 3. To look it up later, run `SELECT flight_id, flight_name FROM MD_LIST_FLIGHTS();`.

## Step 2: Test the Flight from SQL

Before you wire up a button, run the Flight the way the Dive will: with config values for one run. These are the same calls `useRunFlight` and `useFlightRun` make. Store the Flight's ID in a variable, because the `MD_*` Flight functions only accept literals and variables:

```sql
SET VARIABLE decide_order_id = (
    SELECT flight_id FROM MD_LIST_FLIGHTS() WHERE flight_name = 'decide_order'
);
```

Start a run with config values for this run only. The Flight's stored config doesn't change:

```sql
SET VARIABLE run_number = (
    SELECT run_number
    FROM MD_RUN_FLIGHT(
        flight_id := getvariable('decide_order_id'),
        config := MAP {'ORDER_ID': '1042', 'DECISION': 'approved'}
    )
);
```

Check the run until its status is `SUCCEEDED`, `FAILED`, or `CANCELLED`. Older clients report these with a `RUN_STATUS_` prefix, such as `RUN_STATUS_SUCCEEDED`:

```sql
SELECT status, exit_code, started_at, ended_at, config
FROM MD_GET_FLIGHT_RUN(
    flight_id := getvariable('decide_order_id'),
    run_number := getvariable('run_number')
);
```

If it failed, read the output. As the Flight's owner, you can read every run's logs:

```sql
SELECT line
FROM MD_GET_FLIGHT_LOGS(
    flight_id := getvariable('decide_order_id'),
    run_number := getvariable('run_number')
)
ORDER BY line_number;
```

Then confirm the write landed:

```sql
SELECT order_id, status, decided_at
FROM ops.main.orders
WHERE order_id = '1042';
```

Once the Dive is live, every click is a run. To see what viewers did, list the recent runs with who started each one and the values it used:

```sql
SELECT run_number, status, started_by_name, config['ORDER_ID'] AS order_id, config['DECISION'] AS decision, created_at
FROM MD_LIST_FLIGHT_RUNS(flight_id := getvariable('decide_order_id'))
ORDER BY run_number DESC
LIMIT 20;
```

`started_by_name` is `NULL` for scheduled runs and for runs started before this column existed. Only the Flight's owner and the organization's Admins can list its runs.

## Step 3: Add the button to the Dive

### AI agent

Ask your AI agent to add the action to the Dive:

> "Add an **Approve** and a **Reject** button to each pending order in my orders Dive. They should run the `decide_order` Flight with `ORDER_ID` and `DECISION`, show the run's status, and refresh the table when it succeeds."

The agent looks up the Flight's ID, adds the `useRunFlight` and `useFlightRun` hooks, and saves a new version of the Dive.

### Dive code

Use [`useRunFlight`](/sql-reference/motherduck-sql-reference/dives/use-run-flight) to start a run and [`useFlightRun`](/sql-reference/motherduck-sql-reference/dives/use-flight-run) to follow it. Write the Flight's ID into the Dive as a constant:

```jsx
import { useSQLQuery, useRunFlight, useFlightRun } from "@motherduck/react-sql-query";

const DECIDE_ORDER_FLIGHT = "7b1e2c3d-0000-4000-8000-000000000001";

export default function Dive() {
  const orders = useSQLQuery(`
    SELECT order_id, customer, amount
    FROM "ops"."main"."orders"
    WHERE status = 'pending'
    ORDER BY order_id
  `);

  const decide = useRunFlight(DECIDE_ORDER_FLIGHT);
  const latest = useFlightRun(DECIDE_ORDER_FLIGHT, decide.data?.runNumber, {
    onFinished: (run) => {
      if (run.status === "SUCCEEDED") orders.refetch();
    },
  });
  const busy = decide.isPending || latest.isChecking;

  const rows = Array.isArray(orders.data) ? orders.data : [];

  return (
    <div className="p-6">
      {rows.map((row) => (
        <div key={row.order_id} className="flex gap-2 items-center">
          <span>{row.customer}</span>
          <button
            className="rounded border px-3 py-1"
            disabled={busy}
            onClick={() => decide.run({ ORDER_ID: String(row.order_id), DECISION: "approved" })}
          >
            Approve
          </button>
          <button
            className="rounded border px-3 py-1"
            disabled={busy}
            onClick={() => decide.run({ ORDER_ID: String(row.order_id), DECISION: "rejected" })}
          >
            Reject
          </button>
        </div>
      ))}
      {latest.status && <p>Last action: {latest.status}</p>}
      {decide.error && <p>Couldn't start: {decide.error.message}</p>}
      {latest.isFinished && latest.status !== "SUCCEEDED" && (
        <p>The action didn't finish. Try again, or ask the Dive's owner.</p>
      )}
    </div>
  );
}
```

A few rules keep write-back predictable:

- **Run only on a click.** Every call to `run` starts a new run. Don't call it while rendering, in `useEffect`, or on a timer.
- **Pass strings.** Config values must be strings, so convert numbers, dates, and booleans first.
- **Write the Flight's ID into the Dive.** Don't look it up by name inside the Dive: `MD_LIST_FLIGHTS()` lists only the viewer's own Flights (Admins see the organization's), so most viewers would find nothing.
- **A started run isn't a finished run.** `useRunFlight` succeeds when the run starts. Follow it with `useFlightRun`, which checks the run's status until it ends, and refetch in `onFinished`.
- **Show progress and errors.** Disable the button while a run is starting or being checked, and show errors in words the viewer understands.
- **Handle viewers who can't run the Flight.** The button appears for everyone who can view the Dive, and MotherDuck checks access only when the run starts. A viewer without access gets an error in `useRunFlight`'s `error`. If the Dive doesn't show `error`, clicking the button does nothing visible. Show a message such as "You don't have access to run this. Ask the Dive's owner.", and keep the rest of the Dive working.

## Step 4: Show the new data

Refetch the queries that read the tables the Flight writes when `useFlightRun` reports `SUCCEEDED`, as in the example above. A Dive that reads the database directly sees the new rows right away.

![Before and after: on the left, order 1042 is pending with Approve and Reject buttons. On the right, after the run, a banner reads Run #4 succeeded. Orders updated, and order 1042 is approved](img/writable-dives-before-after.png)

Before the click, #1042 is pending and has **Approve** and **Reject** buttons. After the run succeeds, the Dive queries again and gets the row with its new status, so it shows as approved with no buttons, and the banner confirms which run made the change.

If the Dive reads the Flight's output through a [Share](/key-tasks/sharing-data/sharing-overview/), the Share picks up new data automatically within about a minute. When viewers must see it immediately, end the Flight with `CREATE SNAPSHOT OF "<database>"`, then run `REFRESH DATABASE "<alias>"` in the Dive before refetching. Both steps cost performance: `CREATE SNAPSHOT` blocks other writes to the database while it runs, and `REFRESH DATABASE` makes the next queries read cold data. Use them only when the delay matters.

## Step 5: Let others run the Flight

Anyone who can view the Dive can click its buttons, but MotherDuck runs the Flight only for viewers who own it or have `run` access to it. Grant `run` to the people the Dive is for:

```sql
GRANT RUN ON FLIGHT '<flight_id>' TO ROLE order_approvers;
```

Grantees are `USER <username>`, `ROLE <role_name>`, or `ORGANIZATION`, separated by commas. Only the Flight's owner can grant `run`. Admins can list and revoke `run` grants on any Flight in the organization, but can't add them. The `GRANT` statement takes the Flight's ID as a quoted string, not its name. See [`GRANT RUN ON FLIGHT`](/sql-reference/motherduck-sql-reference/flights/grant-run-on-flight) for the full syntax.

Preset roles nest: Admin is a member of Builder, and Builder of Explorer. A grant to `ROLE explorer` reaches every Builder and Admin too.

Grant to several grantees at once, check who has access, and remove a grantee:

```sql
GRANT RUN ON FLIGHT '<flight_id>' TO USER duck, ROLE finance;

SHOW GRANTS ON FLIGHT '<flight_id>';

REVOKE RUN ON FLIGHT '<flight_id>' FROM USER duck;
```

To audit access across several Flights, use the [`MD_LIST_FLIGHT_GRANTEES`](/sql-reference/motherduck-sql-reference/flights/md-list-flight-grantees) table function, which takes the ID as a `UUID`:

```sql
SELECT grantee_name, grantee_type
FROM MD_LIST_FLIGHT_GRANTEES('<flight_id>'::UUID)
ORDER BY grantee_type, grantee_name;
```

A grantee can read only the runs they started, one at a time by run number, with [`MD_GET_FLIGHT_RUN`](/sql-reference/motherduck-sql-reference/flights/md-get-flight-run). This is what `useFlightRun` does. They can't list the Flight's runs with [`MD_LIST_FLIGHT_RUNS`](/sql-reference/motherduck-sql-reference/flights/md-list-flight-runs), and they can't read its logs.

You can also ask your AI agent to grant access. It recommends the grants and waits for you to confirm before it applies them.

A `run` grant lets someone start the Flight, never act as its owner elsewhere. An Admin can see and stop any Flight in the organization, but runs it only with a grant:

| | Owner | Admin | `run` grant | No grant |
|---|---|---|---|---|
| Run the Flight | Yes | With a `run` grant | Yes | No |
| See runs' status, exit codes, and config values | All runs | All runs | Runs they started | No |
| Read its logs or source code | Yes | Yes | No | No |
| Cancel a run | Yes | Yes | No | No |
| Edit or delete it | Yes | No | No | No |
| Manage `run` grants | Add, list, revoke | List, revoke | No | No |

Keep these in mind when you grant access:

- **The Flight always runs as its owner,** with the owner's token, secrets, and database permissions, whoever clicks the button. Grant `run` only on Flights whose writes you're comfortable with anyone in the grant making.
- **Config values are visible in the run history** to the owner and Admins, and to each grantee for the runs they started. Put values that must stay hidden in [secrets](/key-tasks/flights/flights-authentication-config-and-secrets), not config.
- **A grant to the organization includes its service accounts.** To limit a Flight to specific people, grant to a role that has only those people.
- **[Users with no roles](/concepts/roles-and-access-control/#users-with-no-roles) can't run Flights,** even with a grant.

## Where Dive write-back works

Dives can run Flights in the MotherDuck web app, in the MotherDuck MCP Dive viewer, and in the CLI's local preview with [`motherduck dive watch`](/sql-reference/motherduck-cli/dive/). A button returns `This Dive can't run Flights.` in two places:

- **[Embedded Dives](/key-tasks/dives/embedding-dives/)** never run Flights.
- **The MCP Dive viewer, when your AI tool blocks its direct connection** to MotherDuck. The viewer then sends the Dive's queries through the MCP server, which runs only read-only SQL, so runs are off.

## Troubleshooting

| Message or symptom | Cause | Fix |
|---|---|---|
| Nothing happens when a viewer clicks the button. | The run was refused, and the Dive doesn't show `useRunFlight`'s `error`. | Render `error`, so viewers see why. |
| `This Dive can't run Flights.` | The Dive is shown somewhere runs are switched off: an embed, or the MCP Dive viewer when your AI tool blocks its direct connection. | Open the Dive in the MotherDuck web app. |
| A not-found error, or `You can only run or modify Flights that you own. Ask the owner to give you access to run this one.` | The viewer doesn't own the Flight and has no `run` grant. | Ask the owner to [grant `run`](#step-5-let-others-run-the-flight). |
| `Flights are not available for restricted members ...` | The viewer has no roles, or the Dive connects with a read scaling token. | Ask an Admin to assign the viewer a role. |
| `Only a Flight's owner and the admins of its organization can list its runs. ...` | A grantee called `MD_LIST_FLIGHT_RUNS`. | Read the run you started by number with `MD_GET_FLIGHT_RUN`. |
| The run succeeds but the Dive shows old data. | The Dive didn't refetch, or reads a Share that hasn't updated yet. | Call `refetch()` in `onFinished`, and see [Step 4](#step-4-show-the-new-data) for Shares. |
| The run fails right away. | The Flight raised an error, often on an unexpected config value. | The Flight's owner can read the run's logs. See [Monitoring and debugging Flights](/key-tasks/flights/observability/monitoring-and-debugging). |

## Related

- [`useRunFlight`](/sql-reference/motherduck-sql-reference/dives/use-run-flight) and [`useFlightRun`](/sql-reference/motherduck-sql-reference/dives/use-flight-run) — the Dive SDK hooks.
- [`GRANT RUN ON FLIGHT`](/sql-reference/motherduck-sql-reference/flights/grant-run-on-flight) — give others access to run a Flight.
- [Scheduling Flights and managing runs](/key-tasks/flights/scheduling-and-runs) — the run lifecycle and per-run config overrides.


---

## Docs feedback

MotherDuck accepts optional user-submitted feedback about this page at `GET https://motherduck.com/docs/api/feedback/agent`.
For agents and automated tools, feedback submission should be user-confirmed before sending.

URL-encode query parameter values and send a GET request:

```text
GET https://motherduck.com/docs/api/feedback/agent?page_path=%2Fkey-tasks%2Fdives%2Frun-flights-from-dives%2F&page_title=Add%20write-back%20to%20a%20Dive&text=<url-encoded user feedback, max 2000 characters>
```

Optionally append `&source=<url-encoded interface identifier>` such as `claude.ai` or `chatgpt`.

`page_path` and `text` are required; `page_title` and `source` are optional. Responses: `200 {"feedback_id": "<uuid>"}`, `400` for malformed query parameters, and `429` when rate-limited.
