PUNKthe adaptive runtime

//DOCS Workflows

Workflow builder, templates, scheduling, credentials, and endpoints.

Workflows

Punk workflows are multi-step agent jobs (classify, fetch, branch, call tools, notify) defined as a graph and optimized by the same runtime loop that optimizes single chat requests.

Design Rules

  • Workflows are interpreted, never code-generated. The graph is data; there is no prompt-to-code compiler and no generated JavaScript to audit.
  • LLM nodes are real gateway runs. Every llm node goes through Punk's router, so it inherits governance, caches, artifact routing, fingerprinting, and the learning loop.
  • A workflow run owns its workflow.node.* events for review and audit.
  • Scheduling reuses durable jobs. A minute-cadence sweep reads the workflows table fresh each pass, so deleting a workflow always deletes its schedule.

The Workflow IR

A workflow's graph is a WorkflowIR: { version: 1, entryId, nodes }. Graphs are DAGs: cycles and unreachable nodes are rejected at create/update/import time and again before every run.

Nodes read and write a shared variable environment seeded with the run input (also available as input). Expressions (IRExpr) are the only way config reaches that environment:

KindShapeMeaning
literal{ "kind": "literal", "value": ... }A constant.
ref{ "kind": "ref", "path": "triage.json.priority" }Dotted path into the vars environment.
template{ "kind": "template", "template": "{{page.context}}" }String interpolation over vars.

Node Kinds

KindConfigWhat it does
start(none)Entry marker; follows next.
llmpromptTemplate (required), system?, model?, saveAs?Renders the templates over vars and executes a real gateway chat run. Saves {content, json, runId, route, costUsd, savedUsd} to vars[saveAs ?? "llm"]; json is a best-effort parse of the content.
tool_callserverId (required), tool (required), args? (map of name → IRExpr), saveAs?Invokes a tool on a registered external MCP server. Governance-checked first; see below.
web_fetchurlExpr (IRExpr, required), saveAs?Web fetch. Saves {context, tokensSavedEstimate} to vars[saveAs ?? "page"], compact page context instead of raw HTML.
web_acturl (required), intents (required)Opens a temporary governed web session, executes click/type/select/submit intents, saves {sessionId, source, url, results, context}, then closes the session.
choicebranches (required), otherwise?Branches evaluated in order; first match wins, else otherwise, else next, else the run ends. Conditions are a full IRExpr or the shorthands contains: [path, needle], eq: [path, value], truthy: path.
set_varname (required), value (IRExpr, required)Assigns into the vars environment.
transformexpr (IRExpr, required), pick? (string[]), saveAs?Evaluates the expression, optionally picking keys from an object result.
mapitems (IRExpr → array, required), itemVar (required), indexVar?, collectInto (required), body ({entryId, nodes}, required), maxIterations?, continueOnError?Runs the body sub-graph once per item of items in a child scope (itemVar/indexVar bound, never leaked to the parent), collecting each iteration's output into vars[collectInto]. See Map iteration.
notifychannel (webhook | slack, default webhook), target (IRExpr, string, or cred:<id>, required), messageTemplate?, payloadTemplate?Posts to an external destination. slack sends the incoming-webhook {text} shape; webhook posts the rendered JSON payload (else {message}). SSRF-guarded: private destinations always blocked, cred:<id> resolves from the vault, 5s timeout. See Notify channels.
outputvalue (IRExpr, required)Terminates the run with the value.

Execution Limits

  • 100 nodes per run (also the cycle backstop), 120s wall clock.
  • A node failure fails the run. There are no per-node retries; scheduled runs retry whole-run through the job queue (up to 3 attempts), manual/API runs never retry.

Interpreted, Never Code-Generated

Workflows follow the same principle as artifacts: a declarative structure executed by the gateway, never generated or eval'd code. Effects such as LLM calls, MCP tools, web fetches, webhooks, and tracing stay visible node by node.

LLM Nodes Are Gateway Runs

This is the optimization story. Each llm node is a loopback request into Punk's own router with app id workflow:<id> and agent workflow-engine, temperature 0. That means:

  • The node is governed, fingerprinted, and traced like any other run.
  • Repeated node work can hit caches, form patterns, and route through proven optimized paths. The workflow gets cheaper run over run without changing the graph.
  • Per-node cost and savings are read from the persisted child gateway run and roll up onto the workflow run's costUsd/savedUsd.

GET /api/v1/workflows/:id/savings aggregates this across run history: total cost, total saved, and optimizedShare, the share of would-have-been spend the optimizer avoided.

Map Iteration

A map node runs a self-contained body sub-graph once per item of an array expression. Each iteration binds itemVar (and optional indexVar) in a child scope that is a shallow copy of the parent vars, so iteration variables never leak back out; the per-iteration output is collected into vars[collectInto]. Bodies can contain llm and tool_call nodes, so every iteration is a real metered, governed, cacheable gateway run, and child costs/savings roll up onto the workflow run.

Fan-out is bounded on three axes so a map can't run away:

  • maxIterations caps the loop count (default 100, hard ceiling 1000).
  • Body nodes count against the same shared node budget (100) and 120s wall clock as the rest of the run.
  • Map bodies nest to a maximum depth of 3 (a map inside a map inside a map).

continueOnError (default off) collects { error } for a failed item and continues; otherwise the first body failure fails the map node and the whole run. The batch-triage template demonstrates mapping an LLM classification over a list of tickets.

Notify Channels

The notify node posts to an external destination over one of two channels:

  • slack sends the Slack incoming-webhook { text } shape, rendered from messageTemplate (or the default message).
  • webhook posts the rendered payloadTemplate as JSON (falling back to {message} when the template doesn't parse).

The target is an IRExpr, a literal URL string, or a cred:<id> reference resolved from the encrypted vault at run time, so a Slack webhook URL or a signed endpoint never lives in the graph. Either channel is SSRF-guarded under the stricter webhook posture: private/loopback destinations are always blocked, with a 5s timeout.

Builder UI

#/workflows is the built-in visual workflow creator. Start here when you want a multi-step agent job without writing code.

The fastest path:

  1. Open the template gallery.
  2. Pick support-triage, web-research, or pricing-monitor.
  3. Click USE TEMPLATE.
  4. Click RUN.
  5. Paste JSON input and inspect the returned output, cost, savings, and run timeline.

After that, edit the graph. #/workflows lists workflows (enabled toggle, node count, cron badge, last run, per-workflow savings) plus the template gallery. #/workflows/:id is a three-pane editor:

  • Palette: click or drag node-kind chips onto the canvas.
  • Canvas: drag nodes, drag the background to pan, click a node's output port then a target to connect (choice sources add a branch). DEL removes the selected node/edge, ESC cancels a connect, AUTO-LAYOUT does left-to-right topological layering.
  • Inspector: per-kind config forms; IRExpr fields are kind+value selects. Validation errors from a rejected save render inline.

Toolbar: SAVE (dirty indicator), RUN (JSON input panel, synchronous result with a link to the run timeline), EXPORT (downloads the export envelope), DELETE.

Use Agents instead of the workflow editor when the job is just one scheduled prompt. Use Workflows when you need multiple nodes, branching, web fetches, tool calls, notifications, or export/import.

Run Timelines And Node Traces

Every node emits workflow.node.started, then workflow.node.completed ({durationMs, summary}) or workflow.node.failed ({error}) onto the workflow run. The dashboard uses these events to render node timelines.

GET /api/v1/workflow-runs/:id returns the run plus its events. The dashboard renders this as a node timeline at #/workflow-runs/:id; llm node payloads link through to the child gateway run, where the route explanation says whether the node was served live, from cache, or by an artifact.

When a workflow or agent run fails, the run detail page also shows a failed-node summary with the error and any child gateway-run link that can be recovered from the trace payload. The Recovery panel starts a new workflow or agent run with the same stored input; it does not mutate or replay the original trace, so the new attempt gets its own run id, cost accounting, and node ledger.

Scheduling

Set scheduleCron to a 5-field cron expression (UTC). Supported syntax: *, numbers, lists (1,15,30), ranges (9-17), steps (*/5, 10-50/10). Deliberate limits: no month/day names, no L/W/#/? extensions. When both day-of-month and day-of-week are restricted, the date matches if either matches (standard cron).

How it runs: a minute-cadence sweep job reads the scheduled workflows fresh each pass and enqueues a durable run job per match, deduped per workflow per UTC minute. There are no persistent repeatable jobs to clean up. Deleting (or disabling) a workflow is also unscheduling it, with no orphan schedule left behind.

Credentials

Stored credentials hold the secrets that workflow tool calls and MCP servers need:

  • Encrypted at rest with AES-256-GCM inside the store boundary. Key: PUNK_ENCRYPTION_KEY (32 bytes, base64). Authenticated deployments refuse new credential writes until it is set; open dev mode may use the deterministic dev key and warns loudly.
  • The API never returns a secret in any form. POST /api/v1/credentials accepts the secret once and responds with a masked record; lists are masked too.
  • Reference a credential as cred:<id> in MCP server env/header values. It resolves at connect time, tenant-checked, inside the store.

Tool Calls And Governance

A tool_call node executes against a registered external MCP server. Before any connection is made, the gateway classifies the tool's side-effect level and runs the governance check with the workflow's agent identity. Both deny and approval_required fail the node closed. An unattended workflow cannot wait on a human. tool.called/tool.completed events land on the workflow run with the classified side-effect level.

web_fetch nodes use the same SSRF posture as POST /api/v1/web/fetch; notify nodes use the stricter webhook posture (private destinations always blocked).

Export And Import

POST /api/v1/workflows/export returns a { "punkWorkflows": 1, "workflows": [...] } envelope (all workflows, or pass ids to select). POST /api/v1/workflows/import validates every entry before creating anything and rolls back any rows it created if storage fails mid-import. Imports are atomic, and a single invalid graph rejects the whole batch with per-entry errors.

Endpoints

Mutations require admin except the read-only export endpoint. See API for auth and response conventions.

MethodPathPurpose
GET/api/v1/workflowsList workflows.
POST/api/v1/workflowsCreate (graph validated; 400 with per-node errors).
GET/api/v1/workflows/:idRead one.
PATCH/api/v1/workflows/:idUpdate; every accepted PATCH bumps version.
DELETE/api/v1/workflows/:idDelete (also unschedules).
POST/api/v1/workflows/:id/runExecute synchronously; body { input?, trigger? }; 422 if the stored graph is invalid.
GET/api/v1/workflows/:id/runsRun history (?limit=).
GET/api/v1/workflows/:id/savingsCost/savings rollup with optimizedShare.
GET/api/v1/workflow-runs/:idRun plus its node-level trace events.
GET/api/v1/workflow-templatesBuilt-in templates.
POST/api/v1/workflow-templates/:id/instantiateCreate a workflow from a template ({ name? }).
POST/api/v1/workflows/exportExport envelope ({ ids? }).
POST/api/v1/workflows/importAtomic import of an export envelope.
GET/api/v1/credentialsList stored credentials (masked).
POST/api/v1/credentialsStore a credential ({ name, provider, secret }); secret never echoed.
DELETE/api/v1/credentials/:idDelete a credential.

Templates

Built-in templates ship complete, validated graphs and run end-to-end offline against the mock provider:

TemplateWhat it doesInput
support-triageClassify a ticket with an LLM node, branch on priority, webhook high-priority tickets, emit the classification.{ ticket: { subject, description } }
web-researchFetch a page as compact context, summarize it with an LLM node, emit the summary.{ url }
pricing-monitorFetch a pricing page as compact context, extract plans and prices, emit the extraction. Pair with a cron schedule to watch competitors.{ url }
batch-triageMap an LLM classification over a list of tickets, collecting the per-ticket results, and emit the batch. Demonstrates the map node end-to-end offline.{ tickets: [{ subject, description }] }

Instantiate from the dashboard gallery or POST /api/v1/workflow-templates/:id/instantiate.