Download the product examples: API scripts, Power Query, OData flows, test CI files and Power BI connector source. The gateway archive includes its full deployment README.
curl -H "Authorization: Bearer $TOKEN" -d '{"customer":"12345678"}' https://terminal.bank.example/api/v1/flows/customer-lookup
{"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:
[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.
Write a flow
A flow is a WND Terminal macro (macros.md) with a header. The header is the API's contract:
#! 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
patternis checked before anything is sent to the host, and a call that doesn't fit is answered 400 with the input's own description.optionalanddefault "US"make an input optional. - Each output comes back typed.
numbergives a JSON number (thousands commas and currency signs dropped, a trailing minus, CR or DB makes it negative).dategives the text as the screen shows it andNAME_isoas YYYY-MM-DD, read in the order#! dates:says (mdyunless it saysdmyorymd).yesnogives true or false.textis 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" 10fails 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,
failwith 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:
wnd-automate flow run customer-lookup.wndflow --input customer=12345678
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), 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), 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):
press PF3
wait "Type a customer number" 10
Rules for a reset macro:
- End with a
waitfor 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):
- A service account's token. For a program that works on its own.
wnd-gateway new-agent-token NAMEmakes the token and the[mcp.service_accounts.NAME]lines. A service account'sconnections,modeandsandbox_onlyapply 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:
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
datewith its_iso); - every error answer above, the 202 for flows that change data, and the
Idempotency-Keyheader; x-wnd-writes,x-wnd-connectionandx-wnd-sessionon each operation, and a tag for each connection;serversfrom[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 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),
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.callfor each call (the flow, the caller, the outcome, how long it took, the run ID),api.refusedwith the reason, andapi.approval_requested, with"via": "api"and the program's address. Never the inputs or the outputs. The flow's own run isagent.flow, and approvals areagent.approvedandagent.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.