nika: v1
workflow:
id: support-triage
description: "Ticket queue → typed triage → urgent escalation → triage board"
# A NON-thinking local model, deliberately: a thinking model can spend the
# whole `max_tokens` budget in its think block and return before the JSON
# (engine#428). Schema showcases pick a model that answers directly.
model: ollama/llama3.2:3b # local · zero key · swap for groq/llama-3.3-70b (triage wants speed)
const:
queue_path: "examples/fixtures/support-queue.json"
secrets:
oncall_webhook:
source: env
key: ONCALL_WEBHOOK_URL
egress: # sanction the one send · the secret IS the URL
- to: "nika:notify"
host_from_self: true
permits:
tools: ["nika:jq", "nika:notify", "nika:read", "nika:uuid", "nika:write"]
net:
# `host_from_self:` above sanctions the FLOW (the secret may be the URL) —
# it does not grant the capability. The host stays unknown at check, so it
# is judged at RUN against this bound: name the escalation host here or the
# send is refused mid-run, after the tokens are already spent.
http: ["hooks.slack.com"]
fs:
read: ["examples/fixtures/support-queue.json"]
# One board per batch id. `*` matches a single segment and never crosses
# `/`, so the uuid can never steer the write out of out/ — `out/**` would
# let it.
write: ["out/triage-*.json"]
tasks:
batch:
invoke:
tool: "nika:uuid"
args: { version: v7 }
queue:
invoke:
tool: "nika:read"
args: { path: "${{ const.queue_path }}" }
triage:
with:
queue: ${{ tasks.queue.output }}
infer:
max_tokens: 1500 # the whole queue in one call · a ceiling, not UNBOUNDED
prompt: |
Triage every ticket in this queue ·
${{ with.queue }}
For each · classify category and urgency, draft a 2-sentence first reply.
schema:
type: object
additionalProperties: false # a deterministic shape across providers
required: [tickets]
properties:
tickets:
type: array
items:
type: object
additionalProperties: false
required: [id, category, urgency, first_reply]
properties:
id: { type: string }
category: { type: string, enum: [billing, bug, how-to, account, other] }
urgency: { type: string, enum: [low, normal, high, critical] }
first_reply: { type: string }
urgent:
with:
tickets: ${{ tasks.triage.output.tickets }}
invoke:
tool: "nika:jq"
args:
input: "${{ with.tickets }}"
expression: 'map(select(.urgency == "high" or .urgency == "critical"))'
escalate:
with:
urgent: ${{ tasks.urgent.output }}
batch: ${{ tasks.batch.output }}
when: ${{ size(with.urgent) > 0 }}
invoke:
tool: "nika:notify"
args:
channel: webhook
target: "${{ secrets.oncall_webhook }}"
message: "Urgent tickets in triage batch ${{ with.batch }} · ${{ with.urgent }}"
severity: warning
board:
with:
tickets: ${{ tasks.triage.output.tickets }}
batch: ${{ tasks.batch.output }}
invoke:
tool: "nika:write"
args:
# A `.json` artifact takes ONE interpolated value as `content:` and
# lets the engine serialize it. Typing `{ "tickets": ${{ … }} }` by
# hand emits unquoted fields and the board stops being JSON.
path: "out/triage-${{ with.batch }}.json"
content: "${{ with.tickets }}"
create_dirs: true
outputs:
tickets:
value: ${{ tasks.triage.output.tickets }}
description: "The classified queue with drafted first replies"