Review an exception queue on a schedule and only interrupt a human when it matters
Run unattended on a schedule, let a model triage the batch, and pull a human in only when the batch warrants it. ## Why this shape Most operational review is a person reading a queue that is usually fine. The naive automation — alert on everything — gets muted within a fortnight, and the one alert that mattered gets muted with it. Here the model's only authority is deciding whether to interrupt someone. It cannot act. Ambiguity escalates rather than resolving quietly, and the digest is posted on quiet days too, so a flow that has silently stopped running looks different from a flow reporting good news. ## Steps 1. **pull** — reads the exception queue. Empty is a normal outcome, not an error. 2. **summarise** — the model returns `{ needs_human, headline, detail }`. Anything other than an explicit `false` counts as needing a human. 3. **escalate** — suspends for acknowledgement. Skipped when `needs_human` is false. 4. **notify** — posts the digest either way. ## What you need before running it - An endpoint returning a JSON array, or `{ "items": [...] }`. - An LLM API key in an Orvanta variable (default `u/admin/AI_API_KEY`). - Optionally an incoming-webhook URL for the digest. Then attach a schedule to the flow — the template has no schedule of its own, because the right cadence is yours to choose. ## Adapting it - **Queue is a database, not an endpoint:** replace `pull` only. - **Tune the escalation rate:** `criteria` is a flow input written in plain language. Start strict, then loosen it once you trust the digest. - **Several acknowledgers:** raise `required_events` on the suspend block. - **Approve individual items rather than the batch:** move the suspend into a for-loop over `results.pull.items`.
| 1 | { |
| 2 | "summary": "Review an exception queue on a schedule and only interrupt a human when it matters", |
| 3 | "description": "Unattended triage where the model decides whether to interrupt someone, and nothing else.", |
| 4 | "schema": { |
| 5 | "$schema": "https://json-schema.org/draft/2020-12/schema", |
| 6 | "type": "object", |
| 7 | "order": [ |
| 8 | "source_url", |
| 9 | "criteria", |
| 10 | "webhook_url", |
| 11 | "approver", |
| 12 | "auth_header", |
| 13 | "max_items", |
| 14 | "api_key_variable", |
| 15 | "base_url", |
| 16 | "model" |
| 17 | ], |
| 18 | "required": [ |
| 19 | "source_url" |
| 20 | ], |
| 21 | "properties": { |
| 22 | "source_url": { |
| 23 | "type": "string", |
| 24 | "description": "Endpoint returning the exception queue as JSON." |
| 25 | }, |
| 26 | "criteria": { |
| 27 | "type": "string", |
| 28 | "description": "When should a human be interrupted? Plain language.", |
| 29 | "default": "Escalate if any exception is unresolved for more than 24 hours, involves a payment, or repeats more than three times." |
| 30 | }, |
| 31 | "webhook_url": { |
| 32 | "type": "string", |
| 33 | "description": "Incoming webhook for the digest. Leave empty to skip.", |
| 34 | "default": "" |
| 35 | }, |
| 36 | "approver": { |
| 37 | "type": "string", |
| 38 | "description": "Who acknowledges an escalation.", |
| 39 | "default": "admin" |
| 40 | }, |
| 41 | "auth_header": { |
| 42 | "type": "string", |
| 43 | "description": "Optional Authorization header for the queue endpoint.", |
| 44 | "default": "" |
| 45 | }, |
| 46 | "max_items": { |
| 47 | "type": "integer", |
| 48 | "description": "Cap on exceptions reviewed per run.", |
| 49 | "default": 200 |
| 50 | }, |
| 51 | "api_key_variable": { |
| 52 | "type": "string", |
| 53 | "description": "Orvanta variable path holding the model API key.", |
| 54 | "default": "u/admin/AI_API_KEY" |
| 55 | }, |
| 56 | "base_url": { |
| 57 | "type": "string", |
| 58 | "description": "OpenAI-compatible API base URL.", |
| 59 | "default": "https://api.openai.com/v1" |
| 60 | }, |
| 61 | "model": { |
| 62 | "type": "string", |
| 63 | "description": "Model name.", |
| 64 | "default": "gpt-4o-mini" |
| 65 | } |
| 66 | } |
| 67 | }, |
| 68 | "value": { |
| 69 | "modules": [ |
| 70 | { |
| 71 | "id": "pull", |
| 72 | "summary": "Read the exception queue", |
| 73 | "value": { |
| 74 | "type": "rawscript", |
| 75 | "tag": "", |
| 76 | "language": "bun", |
| 77 | "content": "/**\n * Pull the current exception queue.\n *\n * Returns an empty list rather than throwing when there is nothing to do —\n * an unattended flow that errors on a quiet day trains people to ignore its\n * alerts, which defeats the purpose of running it unattended.\n */\nexport async function main(\n source_url: string,\n auth_header = \"\",\n max_items = 200\n): Promise<{ count: number; items: Record<string, unknown>[] }> {\n const headers: Record<string, string> = { Accept: \"application/json\" }\n if (auth_header) headers[\"Authorization\"] = auth_header\n\n const res = await fetch(source_url, { headers })\n if (!res.ok) {\n throw new Error(`Could not read the exception queue: ${res.status} ${await res.text()}`)\n }\n\n const body = await res.json()\n const items = Array.isArray(body) ? body : (body?.items ?? [])\n if (!Array.isArray(items)) {\n throw new Error(\"Expected an array, or an object with an 'items' array\")\n }\n\n // Cap the batch so one bad day cannot produce an unreviewable digest or an\n // enormous prompt.\n return { count: items.length, items: items.slice(0, max_items) }\n}\n", |
| 78 | "input_transforms": { |
| 79 | "source_url": { |
| 80 | "type": "javascript", |
| 81 | "expr": "flow_input.source_url" |
| 82 | }, |
| 83 | "auth_header": { |
| 84 | "type": "javascript", |
| 85 | "expr": "flow_input.auth_header" |
| 86 | }, |
| 87 | "max_items": { |
| 88 | "type": "javascript", |
| 89 | "expr": "flow_input.max_items" |
| 90 | } |
| 91 | } |
| 92 | }, |
| 93 | "timeout": 60, |
| 94 | "retry": { |
| 95 | "exponential": { |
| 96 | "attempts": 3, |
| 97 | "multiplier": 2, |
| 98 | "seconds": 2 |
| 99 | } |
| 100 | } |
| 101 | }, |
| 102 | { |
| 103 | "id": "summarise", |
| 104 | "summary": "Triage the batch and decide whether to interrupt anyone", |
| 105 | "value": { |
| 106 | "type": "rawscript", |
| 107 | "tag": "", |
| 108 | "language": "bun", |
| 109 | "content": "import * as orvanta from \"@orvanta/client\"\n\ntype Review = {\n needs_human: boolean\n headline: string\n detail: string\n}\n\n/**\n * Summarise the batch and decide whether a person needs to look.\n *\n * The model judges severity; it does not get to act. Its only power here is to\n * decide whether to interrupt someone — which is exactly the amount of\n * authority an unattended summariser should have.\n */\nexport async function main(\n batch: { count: number; items: Record<string, unknown>[] },\n criteria: string,\n api_key_variable = \"u/admin/AI_API_KEY\",\n base_url = \"https://api.openai.com/v1\",\n model = \"gpt-4o-mini\"\n): Promise<Review> {\n if (batch.count === 0) {\n return { needs_human: false, headline: \"Nothing in the queue\", detail: \"\" }\n }\n\n const apiKey = await orvanta.getVariable(api_key_variable)\n\n const res = await fetch(`${base_url}/chat/completions`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${apiKey}` },\n body: JSON.stringify({\n model,\n response_format: { type: \"json_object\" },\n messages: [\n {\n role: \"system\",\n content:\n \"You review a batch of operational exceptions. Reply with JSON only, matching \" +\n '{\"needs_human\": <boolean>, \"headline\": <one sentence>, \"detail\": <short markdown>}.'\n },\n {\n role: \"user\",\n content:\n `Escalate to a human when: ${criteria}\\n\\n` +\n `${batch.count} exception(s):\\n${JSON.stringify(batch.items, null, 2)}`\n }\n ]\n })\n })\n\n if (!res.ok) throw new Error(`Model call failed: ${res.status} ${await res.text()}`)\n\n const raw = (await res.json())?.choices?.[0]?.message?.content\n if (typeof raw !== \"string\") throw new Error(\"Model returned no message content\")\n\n let parsed: Review\n try {\n parsed = JSON.parse(raw)\n } catch {\n throw new Error(`Model did not return JSON: ${raw.slice(0, 300)}`)\n }\n\n // Ambiguity escalates. If the model did not clearly say \"no human needed\",\n // a human is needed — the safe default for an unattended flow is to\n // interrupt someone, not to stay quiet.\n parsed.needs_human = parsed.needs_human !== false\n\n return parsed\n}\n", |
| 110 | "input_transforms": { |
| 111 | "batch": { |
| 112 | "type": "javascript", |
| 113 | "expr": "results.pull" |
| 114 | }, |
| 115 | "criteria": { |
| 116 | "type": "javascript", |
| 117 | "expr": "flow_input.criteria" |
| 118 | }, |
| 119 | "api_key_variable": { |
| 120 | "type": "javascript", |
| 121 | "expr": "flow_input.api_key_variable" |
| 122 | }, |
| 123 | "base_url": { |
| 124 | "type": "javascript", |
| 125 | "expr": "flow_input.base_url" |
| 126 | }, |
| 127 | "model": { |
| 128 | "type": "javascript", |
| 129 | "expr": "flow_input.model" |
| 130 | } |
| 131 | } |
| 132 | }, |
| 133 | "timeout": 180 |
| 134 | }, |
| 135 | { |
| 136 | "id": "gate", |
| 137 | "summary": "Interrupt someone only when the batch warrants it", |
| 138 | "value": { |
| 139 | "type": "branchone", |
| 140 | "branches": [ |
| 141 | { |
| 142 | "summary": "The batch needs a human", |
| 143 | "expr": "results.summarise.needs_human", |
| 144 | "modules": [ |
| 145 | { |
| 146 | "id": "escalate", |
| 147 | "summary": "Wait for acknowledgement", |
| 148 | "value": { |
| 149 | "type": "rawscript", |
| 150 | "tag": "", |
| 151 | "language": "bun", |
| 152 | "content": "import * as orvanta from \"@orvanta/client\"\n\n/**\n * Hold the run open until someone acknowledges the batch.\n *\n * Send `urls.approvalPage` wherever your on-call actually looks. The flow does\n * not care which channel that is.\n */\nexport async function main(\n review: { headline: string; detail: string },\n count: number,\n approver = \"admin\"\n) {\n const urls = await orvanta.getResumeUrls(approver)\n return {\n awaiting: approver,\n headline: review.headline,\n detail: review.detail,\n exception_count: count,\n approve_url: urls.approvalPage,\n resume_url: urls.resume,\n cancel_url: urls.cancel\n }\n}\n", |
| 153 | "input_transforms": { |
| 154 | "review": { |
| 155 | "type": "javascript", |
| 156 | "expr": "results.summarise" |
| 157 | }, |
| 158 | "count": { |
| 159 | "type": "javascript", |
| 160 | "expr": "results.pull.count" |
| 161 | }, |
| 162 | "approver": { |
| 163 | "type": "javascript", |
| 164 | "expr": "flow_input.approver" |
| 165 | } |
| 166 | } |
| 167 | }, |
| 168 | "suspend": { |
| 169 | "timeout": 43200, |
| 170 | "required_events": 1, |
| 171 | "resume_form": { |
| 172 | "schema": { |
| 173 | "$schema": "https://json-schema.org/draft/2020-12/schema", |
| 174 | "type": "object", |
| 175 | "order": [ |
| 176 | "acknowledgement" |
| 177 | ], |
| 178 | "required": [], |
| 179 | "properties": { |
| 180 | "acknowledgement": { |
| 181 | "type": "string", |
| 182 | "description": "Optional note recorded with your acknowledgement.", |
| 183 | "default": "" |
| 184 | } |
| 185 | } |
| 186 | } |
| 187 | } |
| 188 | } |
| 189 | }, |
| 190 | { |
| 191 | "id": "ack", |
| 192 | "summary": "Capture the acknowledgement", |
| 193 | "value": { |
| 194 | "type": "rawscript", |
| 195 | "tag": "", |
| 196 | "language": "bun", |
| 197 | "content": "export async function main(\n resume: { acknowledgement?: string } | undefined,\n approver: string\n) {\n return { acknowledged_by: approver, acknowledgement: resume?.acknowledgement ?? \"\" }\n}\n", |
| 198 | "input_transforms": { |
| 199 | "resume": { |
| 200 | "type": "javascript", |
| 201 | "expr": "resume" |
| 202 | }, |
| 203 | "approver": { |
| 204 | "type": "javascript", |
| 205 | "expr": "flow_input.approver" |
| 206 | } |
| 207 | } |
| 208 | } |
| 209 | } |
| 210 | ] |
| 211 | } |
| 212 | ], |
| 213 | "default": [ |
| 214 | { |
| 215 | "id": "no_ack", |
| 216 | "summary": "Nobody needed to be interrupted", |
| 217 | "value": { |
| 218 | "type": "rawscript", |
| 219 | "tag": "", |
| 220 | "language": "bun", |
| 221 | "content": "export async function main() {\n return { acknowledged_by: \"\", acknowledgement: \"\" }\n}\n", |
| 222 | "input_transforms": {} |
| 223 | } |
| 224 | } |
| 225 | ] |
| 226 | } |
| 227 | }, |
| 228 | { |
| 229 | "id": "notify", |
| 230 | "summary": "Post the digest", |
| 231 | "value": { |
| 232 | "type": "rawscript", |
| 233 | "tag": "", |
| 234 | "language": "bun", |
| 235 | "content": "/**\n * Post the digest wherever the team reads it.\n *\n * Runs whether or not a human was involved, so the quiet days are visible too —\n * a monitoring flow that only speaks up on bad days is indistinguishable from\n * one that has silently stopped running.\n */\nexport async function main(\n review: { headline: string; detail: string },\n count: number,\n acknowledged_by: string,\n acknowledgement: string,\n webhook_url: string\n) {\n if (!webhook_url) return { notified: false, reason: \"no webhook_url configured\" }\n\n const res = await fetch(webhook_url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n text:\n `*${review.headline}*\\n` +\n `${count} exception(s) reviewed\\n` +\n (acknowledged_by ? `Acknowledged by ${acknowledged_by}\\n` : \"No escalation needed\\n\") +\n // The acknowledgement form asks for a note; if it were not repeated\n // here, the flow would collect it and silently discard it.\n (acknowledgement ? `Note: ${acknowledgement}\\n` : \"\") +\n (review.detail ? `\\n${review.detail}` : \"\")\n })\n })\n\n if (!res.ok) throw new Error(`Notify failed: ${res.status} ${await res.text()}`)\n return { notified: true, status: res.status }\n}\n", |
| 236 | "input_transforms": { |
| 237 | "review": { |
| 238 | "type": "javascript", |
| 239 | "expr": "results.summarise" |
| 240 | }, |
| 241 | "count": { |
| 242 | "type": "javascript", |
| 243 | "expr": "results.pull.count" |
| 244 | }, |
| 245 | "acknowledged_by": { |
| 246 | "type": "javascript", |
| 247 | "expr": "results.gate.acknowledged_by" |
| 248 | }, |
| 249 | "acknowledgement": { |
| 250 | "type": "javascript", |
| 251 | "expr": "results.gate.acknowledgement" |
| 252 | }, |
| 253 | "webhook_url": { |
| 254 | "type": "javascript", |
| 255 | "expr": "flow_input.webhook_url" |
| 256 | } |
| 257 | } |
| 258 | }, |
| 259 | "retry": { |
| 260 | "constant": { |
| 261 | "attempts": 2, |
| 262 | "seconds": 3 |
| 263 | } |
| 264 | } |
| 265 | } |
| 266 | ] |
| 267 | } |
| 268 | } |