Agent workflows
The full contract for an agent workflow. An LLM agent runs instructions.md as its task, invoking reusable scripts via bash, with its runtime budget shaped by agent.json.
An agent workflow runs at execution time by spawning an LLM agent. instructions.md is its task, a standard operating procedure written in plain markdown. The files under scripts/ are reusable utilities it invokes through bash. agent.json shapes its runtime budget (model, time, effort, pauses).
Reach for an agent workflow when the work is intent-based and variable-length: research, drafting, triage, anything where the exact steps aren't known in advance and the agent must adapt to what it finds. For a fixed, deterministic sequence, use a code workflow.
How it runs
- A trigger fires (webhook, cron, poll) or you start a manual test run.
- The platform boots a sandbox, seeds your workflow files into it, and spawns an LLM agent configured by
agent.json. - The agent reads
instructions.mdas its task, with resolved constants injected into its first message and the current date supplied in context. - It works the procedure: calling APIs through
bashandcurl, running yourscripts/, searching the web, reading files. - It runs until the task is done or
maxTimeMinutesis reached. A human-in-the-loop pause parks the run until the operator answers.
The agent is the reasoning engine. You give it a procedure and tools, not a fixed call graph.
Project layout
workflow.json trigger, inputs, declared credentials, consts
agent.json runtime config: model, time budget, effort, HITL
instructions.md the SOP the agent follows
scripts/ optional reusable utilities, invoked via bash| Path | Required | Purpose |
|---|---|---|
workflow.json | yes | The workflow document: trigger, inputs, declared credentials, consts. |
agent.json | yes | Runtime config for the execution agent (model, time budget, HITL). |
instructions.md | yes | The SOP the execution agent follows. Plain markdown. |
scripts/* | no | Reusable scripts (.sh, .py, .js) the agent invokes via bash. |
main.ts, package.json, and tsconfig.json are forbidden. Those belong to a code workflow; their presence fails validation. Agent workflows have no compile step and install per-run dependencies from bash.
agent.json
{
"model": "vertex/claude-sonnet-4-6",
"maxTimeMinutes": 10,
"effort": "high",
"humanInLoop": { "enabled": false, "instructions": "" },
"executionTitleGuidance": ""
}| Field | Required | Purpose |
|---|---|---|
model | yes | The LLM model id for the runtime agent. |
maxTimeMinutes | yes | Wall-clock cap for one run. |
effort | no | Reasoning effort: low, medium, or high. |
humanInLoop | yes | Whether the agent may pause mid-run to ask the operator something. |
executionTitleGuidance | no | A custom naming prompt for run titles. Usually omit. |
modelis the user's choice, picked at creation. Treat the value already inagent.jsonas deliberate and leave it alone. Don't switch the model based on what the workflow does. The canonical list isCANONICAL_MODEL_MAPinpackages/model-config/src/model.ts.maxTimeMinutes: 5 for simple, 10 standard, 15 or more for research-heavy work.effort: omit if unsure; the model's default applies.humanInLoop: setenabled: trueonly when the agent genuinely needs to pause and ask.instructionsis appended to the agent's system prompt and must name the pause moments concretely ("Pause to confirm the draft email before sending."). Don't enable it "to be safe". A workflow meant to run unattended on its trigger must not pause, and a test of a HITL workflow parks instead of completing, so you can't verify it end to end in one shot. For per-item review where the operator answers each candidate independently, phrase it as "make one parallel ask per item, in the same turn, never a single list-style prompt", so the paginated review widget works.executionTitleGuidance: when set, it replaces the built-in run-title prompt. Usually omit and let runs be named automatically. If you set it, keep the output rules (a short title, roughly six words, output only the title).
instructions.md
Plain markdown the agent reads as its task spec. A common shape:
## Overall Goal
What this workflow accomplishes, in plain language.
## Standard Operating Procedure
Step by step: which endpoints to call and in what order, the field names
and shapes to expect, which scripts to run with what args, how to handle
pagination and rate limits, what to do with the results.
## Important Notes
Edge cases, gotchas, quality rules, lessons from prior test runs.Rules:
- Reference constants by key, not as shell variables. Constants are resolved and injected into the agent's first message under a
Constantssection. Write "send torecipientEmail" and trust the runtime to substitute the value. Never writesend_email.py "recipientEmail"(passes the literal word) orsend_email.py "$recipientEmail"(an unset env var). A const is required unless its schema declares adefault; to make one optional, give its schema a"default"(standard JSON Schema), never a sentinel string like"disabled". - Keep it portable. No credential ids, no user-specific values, no hardcoded dates. Reference credentials by
inputKeyor service, and dates relationally ("today", "the last 24h"). The agent already receives today's date in context. - Write against documented shapes, not pre-walked data. The agent discovers entity shapes, pagination, and associations itself at run time. Describe the endpoints and fields; don't bake in one record's exact association graph from a build-time probe.
- Don't enumerate tool names. Verbatim tool lists go stale the moment a tool is added or renamed, and the agent then hallucinates names from the SOP. Direct the agent at the integration docs instead. Connected services, platform services, and user databases are all reachable from
bashviacurl. - File inputs land on disk. An
inputSchemaproperty with"format": "file"mounts the uploaded file atinputs/<field>/<filename>; the payload value is that path. Tell the agent to read it from there. For multiple files in one field, declare{ "type": "array", "items": { "format": "file" } }— each mounts atinputs/<field>/<index>/<filename>and the value is an array of those paths. - Templated deliverables render through a script.
instructions.mddescribes what goes in each slot; a script underscripts/does the rendering, shipped at build time so the format is fixed run to run. The agent is free to repair or extend that script mid-run; it just shouldn't have to reinvent the format from scratch every run.
Scripts
Scripts are stateless utilities the agent invokes through bash. bash is the invocation surface; the script body can be any language the sandbox runs. They live under scripts/ and the agent calls them by path:
bash work/workflow/scripts/fetch_stories.sh 30
python3 work/workflow/scripts/format_digest.py input.json
node work/workflow/scripts/send_email.mjsScripts read inputs from CLI args or workspace files and write to stdout or files. There is no execute(context) wrapper.
Credentials and platform auth reach a script through environment variables. When the agent runs a bash command and attaches a credential, the platform injects that credential's fields as env vars named $<SERVICE>_<FIELD>_<ID> (uppercased, no cred_ prefix). $PLATFORM_API_KEY and $PLATFORM_BASE_URL are always present. Tell the agent in instructions.md to pass the credential on the bash call and read the injected env var; don't hardcode an id.
Rules:
- Stateless utilities. Orchestration belongs in
instructions.md, not scripts. A script that fetches a list is right; one that loops, decides, and synthesizes is wrong. - One purpose per script. Separate
fetch_stories.sh,format_digest.py,send_email.shrather than one mega-script. - Pass data by path, not by re-typing. If a step wrote data to a file, pass the filename as an arg to the next.
- No
chat-completioncalls. The agent is itself the LLM. - No web-search or scrape scripts, and no web-search credentials (Tavily, Firecrawl). The agent has native web search and page scraping. The SOP tells it to search directly and pass the curated results to a render script.
Scripts are appropriate for calling user-connected services, producing formatted output, fetching from public REST APIs, reusable platform-service utilities, and bulk database operations.
Databases
Database access goes through bash and curl against the platform proxy; the agent has no native SQL tools.
curl -s -X POST "$PLATFORM_BASE_URL/database/user-database" \
-H "Authorization: Bearer $PLATFORM_API_KEY" \
-H "Content-Type: application/json" \
-d '{"databaseId":"udb_abc123","sql":"SELECT * FROM contacts"}'Create and list go through POST $PLATFORM_BASE_URL/database/user-database-admin with { operation, ... }. Use inline curl for simple queries and a script under scripts/ for bulk operations. The full SQL and admin API is documented as operations under the user-database integration; browse it with geni integration.
Worked example
A daily digest workflow. instructions.md drives the procedure; two scripts do the mechanical fetch and render.
## Overall Goal
Post a digest of the day's top Hacker News stories to Slack each morning.
## Standard Operating Procedure
1. Run `bash work/workflow/scripts/fetch_stories.sh 30` to fetch the top 30
story ids and their titles, scores, and urls as JSON on stdout. Save it.
2. Pick the 10 highest-scoring stories. For each, write one sentence on why
it matters.
3. Run `python3 work/workflow/scripts/format_digest.py stories.json` to render
the Slack mrkdwn message.
4. Post the rendered message to the channel in the `slackChannelId` constant,
using the Slack credential. Read its token from the injected env var.
## Important Notes
- If fewer than 10 stories come back, post what you have; don't fail.
- Keep each summary to one sentence.Validating and publishing
geni resource validate checks that agent.json passes its schema, instructions.md is non-empty, and workflow.json is valid. A green validate means it is ready to geni resource publish. Verify the published workflow with geni workflow test. You can switch a workflow between code and agent with geni workflow set-type, then rewrite the files for the new type.
Code workflows
The full contract and runtime API for a code workflow. A single main.ts that runs server-side in Bun once per trigger, with typed credentials, platform auth, logging, and artifact output.
Apps
The full contract for an app. An interactive React SPA in a sandboxed iframe that calls named server-side handlers for data, with a pinned component stack and per-handler caching.