Skip to content

Agent Actions

Agent actions let you call LLMs as first-class workflow steps. Supports 19 LLM providers including Anthropic, OpenAI, Gemini, Groq, and more. Generate text, classify content, extract data, or feed LLM responses directly into downstream steps.

Workers need to be configured with LLM provider API keys. In your worker-config.yaml:

agents:
providers:
- id: anthropic-main
type: anthropic
api_key: "${ANTHROPIC_API_KEY}"
model: claude-opus-4-1-20250805
max_tokens: 2048
temperature: 0.7
max_retries: 2
- id: openai-gpt4
type: openai
api_key: "${OPENAI_API_KEY}"
model: gpt-4o
max_tokens: 1024
# Worker must also declare agent tag
tags: ["script", "agent"]

Server configuration (optional, for validation only):

Optionally define providers in server-config.yaml for YAML validation at load time. The server does NOT store API keys.

agents:
providers:
- id: anthropic-main
type: anthropic
model: claude-opus-4-1-20250805
- id: openai-gpt4
type: openai
model: gpt-4o

In your workflow YAML:

actions:
classify-ticket:
type: agent
provider: anthropic-main
system_prompt: |
You are a support ticket classifier.
Classify tickets as bug, feature_request, or question.
prompt: |
Classify this ticket:
Subject: {{ input.subject }}
Body: {{ input.body }}
output:
category:
type: string
required: true
options: [bug, feature_request, question]
confidence:
type: number
required: true
summary:
type: string
input:
subject: { type: string, required: true }
body: { type: string, required: true }
tasks:
support-workflow:
input:
ticket_subject: { type: string }
ticket_body: { type: string }
flow:
classify:
action: classify-ticket
input:
subject: "{{ input.ticket_subject }}"
body: "{{ input.ticket_body }}"
route-bug:
action: escalate-to-engineering
depends_on: [classify]
when: "{{ classify.output.category == 'bug' }}"
input:
ticket_id: "{{ input.ticket_id }}"
route-feature:
action: add-to-roadmap
depends_on: [classify]
when: "{{ classify.output.category == 'feature_request' }}"
route-question:
action: send-faq-response
depends_on: [classify]
when: "{{ classify.output.category == 'question' }}"

Agent actions execute on workers, which load providers from their worker-config.yaml. The server optionally validates provider names from server-config.yaml.

The following 19 providers are supported:

ProviderTypeDescriptionRequires API Key
AnthropicanthropicClaude modelsYes
AzureazureAzure OpenAI ServiceYes (requires api_endpoint)
CoherecohereCohere modelsYes
DeepSeekdeepseekDeepSeek modelsYes
GaladrielgaladrielGaladriel modelsYes
GeminigeminiGoogle GeminiYes
GroqgroqGroq modelsYes
Hugging FacehuggingfaceHugging Face modelsYes
HyperbolichyperbolicHyperbolic AIYes
LlamafilellamafileLocal Llamafile serverNo
MiramiraMira AIYes
MistralmistralMistral modelsYes
MoonshotmoonshotMoonshot AIYes
OllamaollamaLocal Ollama serverNo
OpenAIopenaiGPT modelsYes
OpenRouteropenrouterOpenRouter APIYes
PerplexityperplexityPerplexity AIYes
TogethertogetherTogether AIYes
xAIxaixAI GrokYes

Anthropic:

agents:
providers:
- id: anthropic-main
type: anthropic
api_key: "${ANTHROPIC_API_KEY}"
model: claude-opus-4-1-20250805
max_tokens: 2048
temperature: 0.7
max_retries: 2

Gemini:

agents:
providers:
- id: gemini-pro
type: gemini
api_key: "${GEMINI_API_KEY}"
model: gemini-2.0-flash
max_tokens: 1024

Groq:

agents:
providers:
- id: groq-fast
type: groq
api_key: "${GROQ_API_KEY}"
model: llama-3.3-70b-versatile
max_tokens: 2048

Ollama (local, no API key required):

agents:
providers:
- id: ollama-local
type: ollama
api_endpoint: "http://localhost:11434"
model: llama2

Azure OpenAI:

agents:
providers:
- id: azure-gpt4
type: azure
api_key: "${AZURE_API_KEY}"
api_endpoint: "https://myresource.openai.azure.com"
model: gpt-4-deployment-name
max_tokens: 2048

OpenAI-compatible endpoint:

agents:
providers:
- id: vllm-local
type: openai
api_key: "${CUSTOM_API_KEY}"
api_endpoint: "http://localhost:8000/v1"
model: local-model
FieldRequiredDescription
idYesUnique provider identifier used in actions
typeYesProvider type (see table above)
api_keyConditionalAPI key for the provider. Not required for ollama and llamafile. Supports env var templating with ${VAR_NAME}
api_endpointConditionalCustom endpoint URL. Required for azure; optional for OpenAI-compatible servers
modelYesModel identifier (e.g., claude-opus-4-1-20250805, gpt-4o, gemini-2.0-flash)
max_tokensNoDefault max completion tokens (can be overridden per action)
temperatureNoDefault sampling temperature (0–2)
max_retriesNoNumber of retries on transient errors (default 2)
FieldRequiredTypeDescription
typeYesStringMust be agent
providerYesStringProvider ID from config
promptYesStringTera template for the user message
system_promptNoStringTera template for system/instruction message
outputNoOutputDefStructured output schema (converted to JSON Schema at dispatch time)
modelNoStringOverride provider’s default model
max_tokensNoIntegerOverride provider’s max tokens
temperatureNoNumberOverride provider’s temperature
inputNoObjectInput schema (same as other actions)
timeoutNoDurationMax time for LLM call (default 5m)

When output is set, it is converted to JSON Schema at dispatch time and the LLM is instructed to respond with JSON matching the schema. If parsing fails, the step fails with an error.

FieldTypeDefaultDescription
typeStringRequiredstring, integer, number, boolean, array, object
descriptionStringHuman-readable description (included in JSON Schema)
requiredBooleanfalseWhether the field must be present in the response
defaultAnyDefault value (included in JSON Schema)
optionsArrayAllowed values (maps to JSON Schema enum)
actions:
analyze:
type: agent
provider: anthropic-main
prompt: "Analyze this data: {{ input.data }}"
output:
sentiment:
type: string
required: true
options: [positive, negative, neutral]
keywords:
type: array
score:
type: number

Output:

{
"sentiment": "positive",
"keywords": ["great", "amazing"],
"score": 0.95,
"_meta": {
"provider": "anthropic",
"model": "claude-opus-4-1-20250805",
"tokens": {
"input": 156,
"output": 42
},
"latency_ms": 1234
}
}
actions:
summarize:
type: agent
provider: anthropic-main
prompt: "Summarize: {{ input.text }}"
# No output — response captured as text

Output:

{
"text": "This is a summary of the provided text...",
"_meta": {
"provider": "anthropic",
"model": "claude-opus-4-1-20250805",
"tokens": {
"input": 128,
"output": 87
},
"latency_ms": 987
}
}

Note: The _meta key is reserved. If your output defines a _meta property, it will be overwritten by the system metadata.

tasks:
workflow:
flow:
analyze:
action: analyze
log-usage:
action: log-api-usage
depends_on: [analyze]
input:
tokens: "{{ analyze.output._meta.tokens.input + analyze.output._meta.tokens.output }}"
latency_ms: "{{ analyze.output._meta.latency_ms }}"

Both prompt and system_prompt are Tera templates. They’re rendered at step execution time with access to:

VariableDescription
input.*Job-level input
<step_name>.output.*Output from completed upstream steps
secret.*Workspace secrets

Example with templating:

actions:
generate-report:
type: agent
provider: anthropic-main
system_prompt: |
You are a report generator for the {{ input.department }} department.
Use a formal, professional tone.
prompt: |
Generate a report for Q{{ input.quarter }} {{ input.year }}.
Previous metrics: {{ previous-step.output.metrics }}
API key for data service: {{ secret.data_api_key }}
input:
department: { type: string }
quarter: { type: integer }
year: { type: integer }

Step name rules: Step names with hyphens become underscores in templates. A step named previous-step is referenced as previous_step.output.*.

Agent actions can fail for several reasons. Each produces a specific error message:

ErrorCauseResolution
Provider '{id}' not foundprovider ID doesn’t exist in configCheck provider ID in action and config
Template rendering error: ...prompt or system_prompt syntax invalidFix Tera template syntax or variable references
LLM API error: ...Network error, API quota exceeded, invalid keyCheck API credentials, rate limits, network connectivity
JSON parsing failed: ...Response doesn’t match outputAdjust schema or prompt to guide LLM response format
Step timeout exceededLLM call took longer than timeoutIncrease timeout or reduce max_tokens

Example: Handling LLM failures with conditions

tasks:
robust-workflow:
flow:
classify:
action: classify-ticket
handle-failure:
action: escalate-to-human
depends_on: [classify]
when: "{{ classify.status == 'failed' }}"
# Runs only if classify step failed
process-result:
action: process-classification
depends_on: [classify]
when: "{{ classify.status == 'completed' }}"
# Runs only if classify succeeded
  • Single-turn only: Agents cannot have multi-turn conversations. Each step is an independent LLM call. Multi-turn support with tools is planned for Phase 7B.
  • No streaming: Responses are fully buffered before being captured. Streaming output is not yet supported.
  • No request/response logging: Full LLM conversation history is not logged (only metadata like token count and latency).

Classify input and route to different steps based on the result:

actions:
classify-issue:
type: agent
provider: anthropic-main
prompt: "Classify this GitHub issue: {{ input.issue_body }}"
output:
category:
type: string
required: true
options: [bug, feature, documentation, wontfix]
tasks:
issue-workflow:
input:
issue_body: { type: string }
flow:
classify:
action: classify-issue
input:
issue_body: "{{ input.issue_body }}"
create-bug-ticket:
action: create-jira-ticket
depends_on: [classify]
when: "{{ classify.output.category == 'bug' }}"
add-feature-label:
action: add-github-label
depends_on: [classify]
when: "{{ classify.output.category == 'feature' }}"

Extract structured data from unstructured text:

actions:
extract-contact:
type: agent
provider: anthropic-main
prompt: |
Extract contact information from this text:
{{ input.text }}
Return as JSON with name, email, phone.
output:
name: { type: string }
email: { type: string }
phone: { type: string }
tasks:
contact-extraction:
flow:
extract:
action: extract-contact
input:
text: "{{ input.unstructured_data }}"
save-contact:
action: save-to-db
depends_on: [extract]
input:
name: "{{ extract.output.name }}"
email: "{{ extract.output.email }}"

Analyze content and conditionally trigger different workflows:

actions:
analyze-sentiment:
type: agent
provider: anthropic-main
prompt: "Analyze sentiment of: {{ input.text }}"
output:
sentiment:
type: string
required: true
options: [positive, negative, neutral]
score: { type: number }
tasks:
feedback-workflow:
input:
feedback: { type: string }
flow:
analyze:
action: analyze-sentiment
input:
text: "{{ input.feedback }}"
positive-branch:
action: send-thank-you
depends_on: [analyze]
when: "{{ analyze.output.sentiment == 'positive' }}"
negative-branch:
action: create-support-ticket
depends_on: [analyze]
when: "{{ analyze.output.sentiment == 'negative' }}"

Workspace secrets are available in prompt templates via {{ secret.NAME }}. When an agent step renders a secret into a prompt, that secret is sent to the external LLM API (Anthropic, OpenAI, or whatever is configured). This is the same access model as script steps (which can read secrets via environment variables), but with agent steps the data leaves your infrastructure.

Recommendations:

  • Only reference secrets in agent prompts when necessary
  • Use the most restrictive LLM provider data policies available
  • Consider using separate, limited-scope API keys as secrets for agent-accessible workflows
  • Review workflow YAML for unintended secret references before deploying

The api_endpoint field in provider configuration accepts arbitrary URLs. Administrators should ensure endpoints point only to trusted LLM-compatible API servers. The field is only configurable via server-config.yaml (not by workflow authors).

Agent steps incur LLM API costs. Keep these strategies in mind:

  • Reduce max_tokens: Lower max_tokens for shorter responses (e.g., classification often needs < 100 tokens)
  • Reuse for multiple steps: Design workflows so one agent step can answer multiple questions, if possible
  • Cache results: For repeated analyses, consider caching outputs in a database step and checking before calling the agent
  • Batch operations: Where possible, batch multiple items into one prompt instead of separate calls
  • Model selection: Use faster/cheaper models for simple tasks; reserve advanced models for complex analysis

Example cost-optimized classification:

actions:
quick-classify:
type: agent
provider: anthropic-main
model: claude-haiku-3-5-20241022 # Faster, cheaper model
max_tokens: 50 # Minimal output
prompt: |
Classify as: [bug, feature, wontfix]
{{ input.title }}