@arcforgelabs/flow 2026.11.1 → 2026.11.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,237 @@
1
+ ---
2
+ name: flow
3
+ description: Run the lab's Flow app and read Seazona through it - invoice tabs, send ready invoices, void or delete drafts, invoice rules, jobs, reconcile Seazona jobs with Flow and Xero, compare prices across Flow, Xero and Seazona, link price codes, review items, the Seazona history import, and Seazona orders, invoices and clients.
4
+ metadata: { "openclaw": { "requires": { "bins": ["curl", "openssl", "sha256sum"] } } }
5
+ ---
6
+
7
+ # Flow
8
+
9
+ Flow is the lab's invoicing app. It takes jobs from Seazona (the lab's case
10
+ system) and sends invoices to Xero. Use `{baseDir}/flow.sh` for every call.
11
+ It holds the Flow key; never print the key file or put the key in a message.
12
+
13
+ Flow address: saved by `setup` next to the key (or `FLOW_URL`). There is no
14
+ default: the key goes only to this lab's own Flow.
15
+ Key file: `FLOW_AGENT_KEY_FILE` (default `~/.openclaw/credentials/flow-agent-key`).
16
+
17
+ ## First time only
18
+
19
+ Run `{baseDir}/flow.sh setup https://<this lab's Flow>`. It saves the address,
20
+ makes the key once and prints only `FLOW_AGENT_KEY_SHA256=<64 hex>`. That digest is safe to share: give it to the
21
+ person who sets up Flow. Flow stores the digest, never the key. Run `setup`
22
+ again any time to see the same digest. To change the key, delete the key file,
23
+ run `setup`, and give the new digest to Flow.
24
+
25
+ Then check: `{baseDir}/flow.sh get me` must answer `HTTP 200`.
26
+
27
+ ## Keep the skill current
28
+
29
+ Run `{baseDir}/flow.sh update` after Flow gets a new release, and when a call
30
+ answers 404 for a route this skill lists. It gets this skill from the Flow it
31
+ talks to, so the skill always matches the release that Flow runs. It changes
32
+ nothing if the answer is not a good skill. Install the skill from GitHub only
33
+ the first time.
34
+
35
+ ## Calls
36
+
37
+ Every answer ends with a line `HTTP <code>`. 401 or 403: the key is not set up
38
+ in Flow, or the Flow person is turned off. Stop and tell the person.
39
+
40
+ Read first. Before a change, read the record, say what you will change, and
41
+ for sends, voids and deletes get the person's yes in the chat.
42
+
43
+ | Do this | Command |
44
+ | --- | --- |
45
+ | Tabs and counts | `flow.sh get 'invoicing/invoices?status=ready&limit=50'` (status: pending, ready, draft, approved, sent, non_chargeable, void) |
46
+ | Search invoices | `flow.sh get 'invoicing/invoices?q=<text>'` |
47
+ | One invoice | `flow.sh get invoicing/invoices/<id>` |
48
+ | Mark ready | `flow.sh post invoicing/invoices/mark-ready '{"invoiceIds":["<id>"]}'` |
49
+ | Send all ready to Xero | `flow.sh post invoicing/invoices/send-ready '{}'` (or `'{"invoiceIds":["<id>"]}'`) |
50
+ | Void an invoice | `flow.sh post invoicing/invoices/<id>/void` |
51
+ | Delete a draft | `flow.sh delete invoicing/invoices/<id>` |
52
+ | Jobs | `flow.sh get 'invoicing/jobs?limit=50'`, one job `invoicing/jobs/<id>/activity` |
53
+ | Invoice rules | `flow.sh get invoicing/rules`; what a rule can use: `flow.sh get invoicing/rule-fields` |
54
+ | Add a rule | `flow.sh post invoicing/rules '{"name":"...","conditions":[{"field":"job_total","test":"at_most","value":0}],"action":{"kind":"wait"}}'` |
55
+ | Change or turn off a rule | `flow.sh put invoicing/rules/<id> '{"enabled":false}'` |
56
+ | Delete a rule | `flow.sh delete invoicing/rules/<id>` |
57
+ | Automations | `flow.sh get invoicing/automations` |
58
+ | Flow settings | `flow.sh get invoicing/settings` |
59
+
60
+ Rule actions: `wait` (hold the job) or `skip` (never invoice it). Take field,
61
+ test and value only from `rule-fields`.
62
+
63
+ Voids: the Void tab holds invoices voided in Flow **or voided or deleted in
64
+ Xero**. Before a clean-up, list the tab, group the rows (Flow void, Xero void,
65
+ deleted draft), show the person the groups, and act only on the group they
66
+ confirm.
67
+
68
+ Sending: Flow sends each invoice to Xero as a draft unless the "Approve and
69
+ email" setting is on (`invoicing/settings`). Say which one applies before a send.
70
+
71
+ ## Prices and price codes
72
+
73
+ Flow's price list is the lab's own list of codes and prices
74
+ (`docs/PRICE_LIST.md`). Flow keeps a copy of the Xero items and the Seazona
75
+ products and links each one to a Flow code. `price-list` reads only Flow's
76
+ copies, so it costs no Xero or Seazona calls.
77
+
78
+ | Do this | Command |
79
+ | --- | --- |
80
+ | Compare all prices | `flow.sh get invoicing/price-list` |
81
+ | Flow codes only | `flow.sh get 'invoicing/price-items?q=<text>'` |
82
+ | Change a Flow price | `flow.sh put invoicing/price-items/<id> '{"unitAmountCents":13500}'` (also `name`, `taxCode` GST or GST_FREE) |
83
+ | Add a Flow code | `flow.sh post invoicing/price-items '{"code":"...","name":"...","unitAmountCents":13500,"taxCode":"GST"}'` |
84
+ | Link records to codes | `flow.sh post invoicing/price-links '{"links":[{"priceItemId":"<id>","system":"xero","externalId":"<id>"}]}'` |
85
+ | Remove a link | `flow.sh delete invoicing/price-links/<link id>` |
86
+ | Accept a difference | `flow.sh post invoicing/price-items/<id>/resolve` |
87
+ | Lines that wait for a code | `flow.sh get invoicing/line-labels` (rows with `unmapped` above 0) |
88
+ | Give a line its code | `flow.sh post invoicing/line-mappings '{"kind":"saved","label":"<label>","unitAmountCents":<label price>,"itemCode":"<Flow code>"}'` |
89
+ | Saved code choices | `flow.sh get invoicing/line-mappings`; change `put invoicing/line-mappings/<id>`, remove `delete invoicing/line-mappings/<id>` |
90
+
91
+ How to read `price-list`:
92
+
93
+ - `systems`: each connected list (`xero`, `seazona`), its record count and
94
+ `syncedAt`. An old `syncedAt` means Flow's copy can be behind.
95
+ - `items`: each Flow code with its `links` (the linked Xero item or Seazona
96
+ product, as `record`) and `findings`. A finding `kind` is `price` (Flow and
97
+ the other system differ: `flow` and `other` in cents), `tax`, `missing` (the
98
+ linked record is gone) or `not_linked` (no Xero item for this code).
99
+ - `unlinked`: Xero items and Seazona products that no Flow code has yet.
100
+
101
+ Reconcile steps:
102
+
103
+ 1. Read `price-list` and `line-labels`. Show the person one table: each code
104
+ with the Flow, Xero and Seazona price, what differs, and each line label
105
+ that waits.
106
+ 2. For each difference, ask which price is right. Do not guess.
107
+ 3. When the person says yes, fix it in the right place:
108
+ - Flow price or code: the Flow commands above.
109
+ - Xero item: your own `xero` command (Flow never changes a Xero item).
110
+ Then refresh Flow's copy of Xero items and contacts:
111
+ `flow.sh post xero/sync '{"organisationId":"<org>"}'`.
112
+ - Seazona product: read only. Tell the person to change it in Seazona.
113
+ - A record that is the same thing as a code but has no link: link it.
114
+ - A difference that is correct (for example a Seazona product at $0):
115
+ accept it with `resolve`.
116
+ 4. Waiting lines: match each label to a code by name and price, show the
117
+ person the pairs, and save each pair they agree with as a line mapping.
118
+ The `label` and `unitAmountCents` come from that `line-labels` row. Jobs
119
+ with that line move on at the next Flow sync (a few minutes).
120
+ 5. Read `price-list` and `line-labels` again and show what is left.
121
+
122
+ Every change on this list (price, code, link, resolve, line mapping) needs
123
+ the person's yes in the chat first, like a send.
124
+
125
+ ## Seazona (read only)
126
+
127
+ Flow reads Seazona for you with its own Seazona connection, so you need no
128
+ Seazona password. Calls share Seazona's limit of 30 a minute with Flow's
129
+ sync: make few calls, and never loop over many records.
130
+
131
+ | Do this | Command |
132
+ | --- | --- |
133
+ | Orders on one day | `flow.sh get 'seazona/api/orders/?ordered=2026-11-11T00:00:00&page=1&pageSize=50'` |
134
+ | One order | `flow.sh get seazona/api/orders/<order id>` |
135
+ | Invoices changed since | `flow.sh get 'seazona/api/invoices/?lastModified=2026-11-01T00:00:00'` |
136
+ | Clients changed since | `flow.sh get 'seazona/api/clients/?lastModified=2026-11-01T00:00:00'` |
137
+ | Products, users | `flow.sh get seazona/api/products/`, `flow.sh get seazona/api/users/` |
138
+
139
+ Answers are `{ "ok": true, "data": ... }`, as Seazona gives them. If the owner
140
+ turned on "Hide patient details", patient, note, file and clinical fields are
141
+ left out. Do not copy patient names outside this chat. 429: wait a
142
+ minute and try once more.
143
+
144
+ ## Reconcile: Seazona jobs, Flow invoices, Xero
145
+
146
+ When the person says "reconcile", "tidy up" or "why is this job not
147
+ invoiced", work in this order. Read first, show the person what you found in
148
+ short groups, then act on the group they confirm.
149
+
150
+ 1. **Jobs against Flow invoices.** `flow.sh get invoicing/reconcile` lists
151
+ jobs with no invoice and invoices with no job. For one row:
152
+ `flow.sh get invoicing/jobs/<job id>/reconcile` or
153
+ `flow.sh get invoicing/invoices/<id>/reconcile` (each lists the likely other
154
+ side; `best: true` is the top guess).
155
+ - Link a job to its invoice: `flow.sh post invoicing/jobs/<job id>/link '{"invoiceId":"<id>"}'`
156
+ - Job needs no invoice: `flow.sh post invoicing/jobs/<job id>/no-invoice`
157
+ - Make the invoice now: `flow.sh post invoicing/jobs/<job id>/draft`
158
+ - Read the job again from Seazona: `flow.sh post invoicing/jobs/<job id>/recheck`
159
+ - **Stuck on a void invoice** (the job shows "Invoice voided", or its Flow
160
+ invoice is void, and nothing drafts): `flow.sh post invoicing/jobs/<job id>/free '{"reason":"<why, in the person's words>"}'`.
161
+ The job lets go of the void invoice and goes on hold, so it does not
162
+ draft by itself. It refuses a live Flow invoice: void that first, only
163
+ after the person says yes to that invoice
164
+ (`flow.sh post invoicing/invoices/<id>/void`).
165
+ - **Held or in review:** `flow.sh get invoicing/jobs/<job id>/activity`
166
+ shows why. Release it: `flow.sh post invoicing/jobs/<job id>/hold '{"state":"none","reason":"<why>"}'`;
167
+ hold it: `'{"state":"held","reason":"<why>"}'` (or `"review"`).
168
+ A `history_import` hold is refused here: use the history import decisions.
169
+ 2. **Flow against Xero.** First get the lab id:
170
+ `flow.sh get invoicing/context` (`organisations[0].id`, below `<org>`).
171
+ - Refresh Flow's copy of Xero: `flow.sh post xero/mirror '{"organisationId":"<org>"}'`
172
+ - Xero invoices against Flow's: `flow.sh get 'xero/import?organisationId=<org>&show=check'`
173
+ (show: `check`, `linked`, `invoice_fits`, `job_fits`, `none`, `all`)
174
+ - Match them all again: `flow.sh post xero/import/match '{"organisationId":"<org>"}'`
175
+ - Link one Xero invoice to a Flow invoice: `flow.sh post xero/import/<xero invoice id>/link '{"organisationId":"<org>","invoiceId":"<id>"}'`
176
+ - Make the Flow invoice for one Xero invoice: `flow.sh post xero/import/<xero invoice id>/make '{"organisationId":"<org>","jobId":"<job id or null>"}'`
177
+ - Xero's state of some Flow invoices: `flow.sh post xero/invoices/status '{"organisationId":"<org>","invoiceIds":["<id>"]}'`
178
+ - Re-check one invoice after a fix in Xero: `flow.sh post xero/invoices/<id>/recheck '{"organisationId":"<org>"}'`
179
+ - Contacts: `flow.sh get 'xero/contacts/match?organisationId=<org>&contactId=<Flow contact id>'`; link to a Xero contact of another name: `flow.sh post xero/contacts/link '{"organisationId":"<org>","contactId":"<id>","xeroContactId":"<id>"}'`
180
+ 3. **Fix the cause with a rule.** If many jobs fail the same way, read
181
+ `invoicing/rule-fields`, propose one rule (`wait` or `skip`), and add it after
182
+ the person says yes. Line names that map to the wrong product:
183
+ `flow.sh get invoicing/line-mappings`, then change one with `put
184
+ invoicing/line-mappings/<id>`. Prices: `flow.sh get invoicing/price-list`.
185
+ 4. **Check Seazona itself** for one job with the Seazona read above (one order).
186
+
187
+ Links, "no invoice", rules and mappings are Flow records only: say what you
188
+ will change and do it after the person's yes. Mirror, match and recheck only
189
+ read Xero. Sending to Xero still goes through `invoicing/invoices/send-ready`.
190
+
191
+ ## Review queue, health and history import
192
+
193
+ Use these to find gaps in past billing: jobs Flow never saw, jobs held from
194
+ the history import, and days where Seazona, Flow and Xero disagree.
195
+
196
+ | Do this | Command |
197
+ | --- | --- |
198
+ | Flow health (build, Seazona sync, daily reconcile, history counts) | `flow.sh health` |
199
+ | Open review items | `flow.sh get 'invoicing/review?limit=100'` (also `kind=`, `state=resolved`) |
200
+ | History import state, backfill and batches | `flow.sh get seazona/history` |
201
+ | One batch, or its rows that wait | `flow.sh get 'seazona/history/batches/<id>?outcome=needs_person'` |
202
+ | Held history jobs that need a person | `flow.sh get 'seazona/history/ledger?state=needs_person'` (no state: all) |
203
+ | Decide batch jobs | `flow.sh post seazona/history/batches/<id>/decisions '{"decisions":[{"jobKey":"<key>","decision":"import"}]}'` (`import`, `skip`, or `null` to undo) |
204
+ | Apply a batch (once only) | `flow.sh post seazona/history/batches/<id>/apply` |
205
+ | Read past Seazona days into Flow | `flow.sh post seazona/history/backfill '{"from":"2026-07-01"}'`; stop: `post seazona/history/backfill/stop` |
206
+ | Let one held history job draft | `flow.sh post seazona/history/ledger/<job id>/release` |
207
+
208
+ How to read them:
209
+
210
+ - `health`: `integrations[].health.history` counts history jobs and open
211
+ batches; `reconcile` shows the last daily check, `daysDifferent` and
212
+ `openItems`; Seazona `missing` lists Seazona records Flow's copy lacks.
213
+ - Review items: `kind` says what differs (for example `reconcile.day`,
214
+ `job.changed`, `job.source_totals`); `evidence` holds the numbers. An item
215
+ closes by itself when its cause is fixed, so fix the cause (link, mapping,
216
+ rule, recheck) and read the list again.
217
+ - History jobs are held with reason `history_import`: they are past work,
218
+ most already billed. A matched one stays held. Release only a job that the
219
+ person agrees was never invoiced.
220
+
221
+ Decisions, apply, backfill and release change Flow records or spend Seazona
222
+ calls: say what you will do and act only after the person's yes. A backfill
223
+ uses the shared Seazona limit a few calls at a time; one at a time only.
224
+
225
+ ## Xero
226
+
227
+ Xero has its own `xero` command on this Gateway, for anything Flow does not
228
+ cover. Flow's send and the reconcile steps above use Flow's own Xero
229
+ connection, not yours.
230
+
231
+ ## Never
232
+
233
+ - Never print, paste or send the key file, a Xero token or a callback URL.
234
+ - Never send, void or delete without the person's yes in this chat.
235
+ - Never send a turned-off person's work again without asking: a send stopped as
236
+ `requester_disabled` means a person turned the assistant off.
237
+ - Never change Flow people, Integrations or owner settings: Flow refuses them.
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env bash
2
+ # Flow command for an AI assistant (docs/AGENT_ACCESS.md, "AI assistant key").
3
+ # flow.sh setup <https://flow url> save this lab's Flow address, make the key
4
+ # once, print only the key's SHA-256
5
+ # flow.sh get <path> GET <FLOW_URL>/api/<path>
6
+ # flow.sh post <path> [json] POST with an optional JSON body
7
+ # flow.sh put <path> <json> PUT with a JSON body
8
+ # flow.sh delete <path> DELETE
9
+ # flow.sh health GET <FLOW_URL>/health (public, no key sent)
10
+ # flow.sh update replace this skill with the copy the
11
+ # saved Flow serves (its own release)
12
+ # The key stays in FLOW_AGENT_KEY_FILE (mode 600). It never goes on a command
13
+ # line or into the output.
14
+ set -euo pipefail
15
+
16
+ KEY_FILE="${FLOW_AGENT_KEY_FILE:-$HOME/.openclaw/credentials/flow-agent-key}"
17
+ URL_FILE="${KEY_FILE}.url"
18
+
19
+ die() { echo "flow.sh: $*" >&2; exit 2; }
20
+
21
+ # The key goes only to the Flow address saved at setup (or FLOW_URL). There is
22
+ # no default address, so a key never goes to another lab's Flow.
23
+ flow_url() {
24
+ local url="${FLOW_URL:-}"
25
+ [[ -n "$url" || ! -s "$URL_FILE" ]] || url="$(tr -d '\n' < "$URL_FILE")"
26
+ [[ "$url" =~ ^https://[A-Za-z0-9.-]+(:[0-9]+)?/?$ ]] || die "no Flow address: run flow.sh setup https://<your Flow>"
27
+ printf '%s' "${url%/}"
28
+ }
29
+
30
+ setup() {
31
+ local url="${1:-}"
32
+ if [[ -n "$url" ]]; then
33
+ [[ "$url" =~ ^https://[A-Za-z0-9.-]+(:[0-9]+)?/?$ ]] || die "the Flow address must be https://<host>"
34
+ mkdir -p "$(dirname "$URL_FILE")"
35
+ printf '%s\n' "${url%/}" > "$URL_FILE"
36
+ fi
37
+ echo "Flow address: $(flow_url)"
38
+ if [[ ! -s "$KEY_FILE" ]]; then
39
+ mkdir -p "$(dirname "$KEY_FILE")"
40
+ (umask 077 && openssl rand -hex 32 > "$KEY_FILE")
41
+ fi
42
+ chmod 600 "$KEY_FILE"
43
+ echo "FLOW_AGENT_KEY_SHA256=$(tr -d '\n' < "$KEY_FILE" | sha256sum | cut -d' ' -f1)"
44
+ }
45
+
46
+ call() {
47
+ local method="$1" path="${2:-}" body="${3:-}"
48
+ [[ -n "$path" ]] || die "give a path, for example: get invoicing/invoices?status=ready"
49
+ [[ -s "$KEY_FILE" ]] || die "no key yet: run flow.sh setup https://<your Flow> first"
50
+ local args=(-sS -X "$method" -K - -H 'Accept: application/json' -w '\nHTTP %{http_code}\n')
51
+ if [[ -n "$body" ]]; then args+=(-H 'Content-Type: application/json' --data-binary "$body"); fi
52
+ printf 'header = "Authorization: Bearer %s"\n' "$(tr -d '\n' < "$KEY_FILE")" \
53
+ | curl "${args[@]}" "$(flow_url)/api/${path#/}"
54
+ }
55
+
56
+ # Fetch SKILL.md and flow.sh from the saved Flow, so the skill always matches
57
+ # the release that Flow runs. Both files arrive first, then both replace the
58
+ # old ones; a bad answer changes nothing.
59
+ update() {
60
+ [[ -s "$KEY_FILE" ]] || die "no key yet: run flow.sh setup https://<your Flow> first"
61
+ local dir tmp file code
62
+ dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
63
+ tmp="$(mktemp -d "$dir/.update.XXXXXX")"
64
+ trap "rm -rf '$tmp'" EXIT
65
+ for file in SKILL.md flow.sh; do
66
+ code="$(printf 'header = "Authorization: Bearer %s"\n' "$(tr -d '\n' < "$KEY_FILE")" \
67
+ | curl -sS -K - -o "$tmp/$file" -w '%{http_code}' "$(flow_url)/api/agent-skill/flow/$file")" \
68
+ || die "could not reach $(flow_url)"
69
+ [[ "$code" == 200 && -s "$tmp/$file" ]] || die "$file: HTTP $code from $(flow_url); nothing changed"
70
+ done
71
+ head -n 1 "$tmp/flow.sh" | grep -q '^#!/usr/bin/env bash' && bash -n "$tmp/flow.sh" \
72
+ || die "flow.sh from $(flow_url) is not a valid script; nothing changed"
73
+ grep -q '^name: flow$' "$tmp/SKILL.md" || die "SKILL.md from $(flow_url) is not the flow skill; nothing changed"
74
+ chmod 755 "$tmp/flow.sh"
75
+ chmod 644 "$tmp/SKILL.md"
76
+ mv -f "$tmp/SKILL.md" "$dir/SKILL.md"
77
+ mv -f "$tmp/flow.sh" "$dir/flow.sh"
78
+ echo "Skill updated from $(flow_url)"
79
+ }
80
+
81
+ case "${1:-}" in
82
+ setup) setup "${2:-}" ;;
83
+ get) call GET "${2:-}" ;;
84
+ post) call POST "${2:-}" "${3:-}" ;;
85
+ put) [[ -n "${3:-}" ]] || die "put needs a JSON body"; call PUT "${2:-}" "$3" ;;
86
+ delete) call DELETE "${2:-}" ;;
87
+ health) curl -sS -H 'Accept: application/json' -w '\nHTTP %{http_code}\n' "$(flow_url)/health" ;;
88
+ update) update ;;
89
+ *) sed -n '2,12p' "$0"; exit 2 ;;
90
+ esac