Docs
Resource

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

  1. A trigger fires (webhook, cron, poll) or you start a manual test run.
  2. The platform boots a sandbox, seeds your workflow files into it, and spawns an LLM agent configured by agent.json.
  3. The agent reads instructions.md as its task, with resolved constants injected into its first message and the current date supplied in context.
  4. It works the procedure: calling APIs through bash and curl, running your scripts/, searching the web, reading files.
  5. It runs until the task is done or maxTimeMinutes is 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
PathRequiredPurpose
workflow.jsonyesThe workflow document: trigger, inputs, declared credentials, consts.
agent.jsonyesRuntime config for the execution agent (model, time budget, HITL).
instructions.mdyesThe SOP the execution agent follows. Plain markdown.
scripts/*noReusable 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": ""
}
FieldRequiredPurpose
modelyesThe LLM model id for the runtime agent.
maxTimeMinutesyesWall-clock cap for one run.
effortnoReasoning effort: low, medium, or high.
humanInLoopyesWhether the agent may pause mid-run to ask the operator something.
executionTitleGuidancenoA custom naming prompt for run titles. Usually omit.
  • model is the user's choice, picked at creation. Treat the value already in agent.json as deliberate and leave it alone. Don't switch the model based on what the workflow does. The canonical list is CANONICAL_MODEL_MAP in packages/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: set enabled: true only when the agent genuinely needs to pause and ask. instructions is 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 Constants section. Write "send to recipientEmail" and trust the runtime to substitute the value. Never write send_email.py "recipientEmail" (passes the literal word) or send_email.py "$recipientEmail" (an unset env var). A const is required unless its schema declares a default; 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 inputKey or 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 bash via curl.
  • File inputs land on disk. An inputSchema property with "format": "file" mounts the uploaded file at inputs/<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 at inputs/<field>/<index>/<filename> and the value is an array of those paths.
  • Templated deliverables render through a script. instructions.md describes what goes in each slot; a script under scripts/ 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.mjs

Scripts 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.sh rather 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-completion calls. 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.

On this page