# Screen flows as REST APIs

Source: https://wndsoftware.com/docs/flow-apis

### Screen flows as REST APIs

A flow is a tested procedure on a host screen, written once by someone who knows the screen. On the gateway (WND Terminal in a browser, see [gateway configuration](https://wndsoftware.com/docs/gateway-configuration)) every flow can also be an HTTPS endpoint. A program sends the inputs as JSON and gets the outputs back as JSON, typed. The gateway describes all of them in an OpenAPI 3.1 document that Postman, Insomnia and code generators import. Power Automate imports the same flows as Swagger 2.0, from `/api/v1/openapi.json?format=swagger2`. The host's programs don't change.

[Download the product examples](https://wndsoftware.com/downloads/wnd-product-examples.zip): API scripts, Power Query, OData flows, test CI files and Power BI connector source. The gateway archive includes its full deployment README.


```text
curl -H "Authorization: Bearer $TOKEN" -d '{"customer":"12345678"}' https://terminal.bank.example/api/v1/flows/customer-lookup
```


```text
{"ok":true,"flow":"customer-lookup","outputs":{"name":"ACME TRADING CO","status":"ACTIVE","opened":"03/14/2019","opened_iso":"2019-03-14","balance":12480.55},"ms":812,"run":"r_8fFUana0gY1kP7hD"}
```

Your license needs to include Screen flows as APIs.


### Turn it on

In the gateway's configuration:


```text
[api]
enabled = true
flows_folder = "/srv/wnd/api-flows"
connection = "Production (CICS)"
```

Put your flows (`.wndflow` files) in `flows_folder`. Flows in the policy's AgentFlowFolders  are served too. The gateway looks at the folders every 10 seconds and reads them again when a file changes, so a new or changed flow is served without a restart. A file it can't read is in the gateway's log (`api.flows`) and in `/api/v1/health`, with the line and what's wrong.

Each flow runs in a connection from the policy. A flow names its own with a `#! connection: Production (CICS)` line. One without that line runs in `[api] connection`. The rest of the settings are in [gateway configuration](https://wndsoftware.com/docs/gateway-configuration#api).


### Write a flow

A flow is a WND Terminal macro ([macros.md](https://wndsoftware.com/docs/macros)) with a header. The header is the API's contract:


```text
#! name: customer-lookup
#! description: Looks up a customer by their 8-digit number: name, status, when the account was opened, and the balance.
#! input customer "The 8-digit customer number" pattern [0-9]{8}
#! output name "The customer's name"
#! output status "ACTIVE, CLOSED or HOLD"
#! output opened "When the account was opened" date
#! output balance "The balance" number
#! writes: no
#! session: pooled
#! reset: back-to-inquiry
#! timeout: 30
wait "Type a customer number" 10
move 6 18
type {customer}
press Enter
wait "STATUS:" 10
if screen has "NOT FOUND"
  fail "No customer has the number " & customer
end
read name 8 10 30
read status 9 10 10
read opened 10 10 10
read balance 11 10 20
```

- The name  is the endpoint: `POST /api/v1/flows/customer-lookup`.
- Each input  is a property of the JSON body. A `pattern` is checked before anything is sent to the host, and a call that doesn't fit is answered 400 with the input's own description. `optional` and `default "US"` make an input optional.
- Each output  comes back typed. `number` gives a JSON number (thousands commas and currency signs dropped, a trailing minus, CR or DB makes it negative). `date` gives the text as the screen shows it and `NAME_iso` as YYYY-MM-DD, read in the order `#! dates:` says (`mdy` unless it says `dmy` or `ymd`). `yesno` gives true or false. `text` is the default.
- writes  says whether the flow changes data on the host. It decides what the API does with it (below).
- timeout  is how long the whole run may take, in seconds (120 unless it says).

Write a flow for programs the way you'd want a colleague to work an unfamiliar screen:

- Always wait for the screen you expect  before you type. `wait "Type a customer number" 10` fails fast and clearly when the session isn't where the flow thinks it is.
- Check what you can.  `assert row 1 has "CUSTOMER INQUIRY" "Not on the inquiry screen"` turns a wrong screen into an answer that says so, instead of outputs read from the wrong place.
- Say "not found" on purpose.  When the host says a customer doesn't exist, `fail` with your words. The caller gets a 422 with your message and the line. That's an answer, not an error, and it's easy for a program to handle.
- Read the outputs last , after the screen that has them, and give each one a type.

Try a flow before it goes on the gateway. With WND Terminal open on the screen it starts from:


```text
wnd-automate flow run customer-lookup.wndflow --input customer=12345678
```


```text
customer-lookup: done in 812 ms (9 steps, 2 screens)
  balance     12480.55
  name        ACME TRADING CO
  opened      03/14/2019
  opened_iso  2019-03-14
  status      ACTIVE
Steps:
  line  12     102 ms  wait "Type a customer number" 10
  ...
```

It runs the flow through the desktop app's automation API (see [automation-api.md](https://wndsoftware.com/docs/automation-api)), in the first open session or the one `--session` names. It's read-only key by key unless the flow says `writes: yes`, as on the gateway. When the run fails it says where, why, and shows the screen then, with sensitive values hidden. `--json` prints the whole outcome: each line's time, the screens it went through, the failure.


### Sessions

How a flow gets its session is in its header.

`#! session: fresh`  (the default). Each call opens a session, signs on (PassTicket sign-on works as for people and agents, see [gateway configuration](https://wndsoftware.com/docs/gateway-configuration#signing-on-to-hosts-with-passtickets)), runs the flow, and closes the session. Simple, and nothing carries over from one call to the next.

`#! session: pooled`  with `#! reset: back-to-inquiry` . The gateway keeps sessions open between calls. A call takes an idle session, runs the flow, and the session goes back to its start screen with the reset macro, ready for the next call. That saves the sign-on and the screens before the start screen, which is most of the time a short flow takes. The reset macro is a plain macro in the flows folder, named as the header says, with `.wndmacro` or `.txt` after it (`back-to-inquiry.wndmacro`):


```text
press PF3
wait "Type a customer number" 10
```

Rules for a reset macro:

- End with a `wait` for something only the start screen has. That's how the gateway knows the session is ready.
- Keep it to moving around (PF3, Clear, a menu choice). It runs with no inputs.
- Make it work from every screen the flow can end on, including after a `fail`.

When a run goes wrong (a timeout, the host closing, a mistake in the macro) or the reset doesn't reach the start screen, the gateway closes that session and never uses it again. The next call opens a new one. A run that ends with the flow's own `fail` or `assert` is a normal answer, so the reset runs and the session stays in the pool when the reset works. Without a `reset` line, a pooled session's next call starts wherever the last one ended.

Pooled sessions are shared only by flows that start on the same screen: flows with the same `reset` macro share their sessions, and a flow without a `reset` line has sessions of its own. A session one flow left on its start screen is never handed to a flow that starts somewhere else.

Limits.  Each caller (a person, or a service account) has its own sessions for each connection and start screen, at most `max_sessions_per_connection` (4). When they're all busy a call waits, at most `queue_timeout_seconds` (30), with at most `max_queue` (50) calls waiting. Past that it's answered 503 with `Retry-After`. An idle pooled session closes after `pool_idle_minutes` (15). Each run is limited to the flow's `timeout`. The person's own session limits (`[limits] max_sessions_per_user`) count API sessions too.

Every session the API opens is recorded, whatever the recording policy says, named with the person or service account. A person's API sessions show in their Your open sessions with an "API" badge.


### Who can call

Each request has `Authorization: Bearer` with a token, as for AI agents on the gateway (see [mcp.md](https://wndsoftware.com/docs/mcp#who-the-agent-works-for)):

- A service account's token.  For a program that works on its own. `wnd-gateway new-agent-token NAME` makes the token and the `[mcp.service_accounts.NAME]` lines. A service account's `connections`, `mode` and `sandbox_only` apply to its API calls too.
- A person's token  from your identity provider. The flow runs as that person, in their sessions, with their limits, and the audit log has it under their name.

Requests from another site's web page are refused, so a page someone opens can't call the API with their browser. The form page (below) is the one part of the API for people in a browser.


### What may run

A flow with `writes: no` runs in every mode, and each key it presses is still checked. A key the screen says could change data stops the run (409, kind `read_only`).

A flow with `writes: yes` follows `[api] mode`, the policy's AgentActionsMode  and the service account's own `mode`. The stricter one wins.

On a small screen, swipe the table to read all columns.


| Mode | A flow that changes data |
| --- | --- |
| `readonly` (the default) | Refused with 409, kind `read_only`. Nothing is sent to the host. |
| `ask` | The call answers 202 at once. The change waits for an approver, then runs. |
| `allow` | Runs. |


#### Approvals

In `ask` mode the gateway takes a session, puts the change in the approvals queue and answers:


```text
HTTP/1.1 202 Accepted
Location: /api/v1/runs/r_30YiUxQWAuIppA2g

{"ok":true,"status":"waiting_approval","flow":"update-customer","run":"r_30YiUxQWAuIppA2g","approval":17}
```

People in `[mcp] approvers_group` see it at `/approvals`, as they see agents' changes: who it's for, the flow and its inputs (sensitive values masked), and the screen as it is, masked. When it's approved the flow runs in the session that was kept for it. `GET` the `Location` for the result. It answers 202 while the run waits or runs, then what the call would have answered (200, 422, ...). A denied run, or one nobody answered within `approval_timeout_seconds`, is answered 403 with `"status":"denied"`. Without an approvers group, a person approves their own calls. A service account's calls need an approvers group.


### Answers

On a small screen, swipe the table to read all columns.


| Status | When | Body |
| --- | --- | --- |
| 200 | The flow ran. | `{"ok":true,"flow":"…","outputs":{…typed…},"ms":812,"run":"r_…"}` |
| 202 | A flow that changes data waits for an approver (`ask` mode). | `{"status":"waiting_approval","run":"r_…","approval":17}`, `Location: /api/v1/runs/r_…` |
| 202 | A flow that changes data didn't reach the approvals queue in time, because it was still waiting for a session. `GET` the `Location` for the result. | `{"status":"running","run":"r_…"}`, `Location: /api/v1/runs/r_…` |
| 400 | An input is missing, doesn't fit its pattern, or isn't one of the flow's. The body isn't a JSON object. | `{"ok":false,"error":{"kind":"bad_input","message":"customer \"12\" doesn't fit: The 8-digit customer number."}}` |
| 401 | No token, or one the gateway doesn't accept. | kind `unauthorized`, with `WWW-Authenticate: Bearer` |
| 403 | Another site's page, a license without Screen flows as APIs, a connection the service account may not use, or an approver said no. | kind `forbidden`, `license`, `refused` or `denied` |
| 404 | No flow by that name (or the API isn't turned on). | kind `not_found` |
| 409 | The flow changes data and the mode is `readonly`, or a read-only flow pressed a key that could change data. An Idempotency-Key in use. | kind `read_only`, `in_progress` or `idempotency_key_reused` |
| 422 | The flow ended with its own `fail`, or an `assert` wasn't true. | kind `fail` or `assert`, such as `{"ok":false,"error":{"kind":"fail","line":14,"message":"No customer has the number 99999999"},"ms":…,"run":"r_…"}` |
| 429 | Too many wrong tokens from this address. Wait a minute, then try again. | kind `busy` |
| 500 | The host went wrong, or the macro has a mistake. | kind `host_error` or `script`, with the `line` and the `screen` then, sensitive values hidden |
| 503 | Every session is busy and the queue is full, or a call waited `queue_timeout_seconds`. | kind `busy`, with `Retry-After: 5` |
| 504 | The host didn't show what the flow waited for within its `timeout`. | kind `timeout` |

Every answer from a run has its `run` ID. `GET /api/v1/runs/{id}` gives it again for an hour, to the caller who made it.


#### Idempotency keys

Send `Idempotency-Key` with any unique text (a UUID) on every call of a flow that changes data. A call with the same key within 24 hours gets the same answer again, with `Idempotent-Replayed: true`, and the flow doesn't run again. So a program that lost the answer (a timeout, a dropped connection) can send the call again safely. The same key with other inputs is answered 409, kind `idempotency_key_reused`. A call answered 503 can be sent again with its key.

Inputs that don't fit are answered 400 before an Idempotency-Key is kept, so nothing is kept for that call. Each caller keeps at most 1,000 keys and run results. When a caller goes past that, their oldest finished ones go first.


### The OpenAPI document

`GET /api/v1/openapi.json` (with a token) is an OpenAPI 3.1 document made from the flows served now:

- one `POST /flows/{name}` operation for each flow, its operation ID the name with underscores (`customer_lookup`), its description the flow's;
- the request body from the inputs, with their descriptions, patterns and defaults;
- the 200 answer from the outputs, typed (a `date` with its `_iso`);
- every error answer above, the 202 for flows that change data, and the `Idempotency-Key` header;
- `x-wnd-writes`, `x-wnd-connection` and `x-wnd-session` on each operation, and a tag for each connection;
- `servers` from `[api] public_url`, the title from `[api] title` ("your company mainframe API" from the license unless you set one), and bearer authentication.

Power Automate's, Power Apps' and Logic Apps' custom connectors import Swagger 2.0, so the gateway also gives the same flows in that form at `/api/v1/openapi.json?format=swagger2`. Each flow becomes an action, with the token as an API key in the `Authorization` header (`Bearer` and the token).

The document has an `ETag`, and changes when a flow file does. `GET /api/v1/flows` lists the same flows as JSON, and `GET /api/v1/health` gives the flows loaded, the problems, and the sessions by connection (idle, busy, broken, waiting, dropped).

[examples/api](https://wndsoftware.com/downloads/wnd-product-examples.zip) has `curl.sh`, `python.py`, a Postman collection, and how to make a Power Automate custom connector from the document (`powerautomate.md`).


### The form page

`https://<gateway>/api/v1/flows/customer-lookup/form` is the flow as a web page: its inputs as fields, a Run button, and the outputs as a table. Anyone signed in to WND Terminal in the browser can use it, with no terminal and nothing to install. A flow becomes a small application for people who don't know the screens. Add `?screen=modern` and the page also shows the last screen as a form, the way Modern view shows it in WND Terminal.

The page runs the flow as the person who is signed in, with the same rules, approvals and audit as the API. A program with a token gets 404 here. It uses the API instead.


### The sandbox

With `[mcp] sandbox_only` (or a service account's `sandbox_only`) the API's sessions play recordings from `[mcp] sandbox_folder` on replay hosts on the gateway's own computer (see [replay-host.md](https://wndsoftware.com/docs/replay-host)), never a real host. The flows in that folder are served too. A flow plays the recording its `#! replay: name.wndrec` line names, or the one named like its connection, or the folder's only recording.

`examples/api` is a sandbox that works as it is. Point `sandbox_folder` at it and call `customer-lookup` (`examples/api/gateway.toml` has the lines). Its recording goes round the customer inquiry many times, so the pooled session and its reset macro work as they would on a host.


### What's recorded

- The audit log.  `api.call` for each call (the flow, the caller, the outcome, how long it took, the run ID), `api.refused` with the reason, and `api.approval_requested`, with `"via": "api"` and the program's address. Never the inputs or the outputs. The flow's own run is `agent.flow`, and approvals are `agent.approved` and `agent.denied`, as for agents. The same events go to the gateway's log for your SIEM.
- Recordings.  Every session the API opens, as above.

What leaves the gateway goes through masking, with one deliberate exception. The outputs are given to the caller as the host shows them, because an API that hides the balance it was asked for is no use. The caller is authenticated, and the flow decides what it reads. Password (hidden) fields can never be read by a flow. Your MaskPatterns , and card numbers and similar values, are still hidden in the screens of error answers, on the approvals page, in the logs and in the form page's screen.

Technical reviewer: [Albert Joseph](https://wndsoftware.com/authors/albert-joseph), WND Software.
