.oaf LanguageThis is the complete reference for the OpenAgentFlow domain-specific language. Every syntax feature, block type, type system construct, and validation rule is documented here.
.oaf//[a, b, c,]// This is a comment
workflow "My Workflow" {
// Everything lives inside a single workflow block
}
Every .oaf file contains exactly one workflow declaration:
workflow "<name>" {
state { ... } // Optional (at most one)
config { ... } // Optional (at most one)
agent <Id> { ... } // Required (one or more)
flow { ... } // Required (exactly one)
}
Blocks can appear in any order within the workflow.
| Rule | Constraint |
|---|---|
| Workflow name | Non-empty quoted string |
state block |
At most one |
config block |
At most one |
agent blocks |
At least one |
flow block |
Exactly one |
A quoted string literal:
workflow "Customer Feedback Analysis" { ... }
Unquoted identifiers matching [A-Za-z_][A-Za-z0-9_-]*:
agent SentimentAnalyzer { ... }
agent my_agent { ... }
Note: Identifiers may contain hyphens (
-) as long as they don’t form the arrow operator (->).
The following cannot be used as agent names:
| Reserved | Purpose |
|---|---|
start |
Flow graph entry point |
end |
Flow graph termination point |
workflow |
Keyword |
agent |
Keyword |
state |
Keyword |
flow |
Keyword |
config |
Keyword |
The state block defines shared variables accessible to all agents during execution.
state {
<name>: <type> [@option [(args)]]
...
}
state {
request: string @required @description("User's initial request")
source_text: string @required
key_points: list[string]
summary: string
score: float @default(0.0)
metadata: map[string, string]
tags: list[string] @reducer("append")
count: int @min(0) @max(100)
}
OAF supports six types:
| Type | Description | Example |
|---|---|---|
string |
Text value | name: string |
int |
Integer number | count: int |
float |
Floating-point number | score: float |
bool |
Boolean value | done: bool |
list[T] |
Ordered collection of type T |
items: list[string] |
map[K, V] |
Key-value mapping | data: map[string, int] |
Generic types can be nested:
state {
matrix: list[list[int]]
nested_map: map[string, list[string]]
}
State variables support optional decorators prefixed with @. These are placed after the type:
state {
request: string @required @description("Initial request")
}
| Option | Arguments | Description |
|---|---|---|
@required |
0 | Marks the variable as required before workflow execution begins |
@default(value) |
Exactly 1 | Provides a default initial value if not provided |
@description("text") |
Exactly 1 | Human-readable description for introspection or prompting |
@desc("text") |
Exactly 1 | Shorthand for @description |
@reducer("strategy") |
Exactly 1 | Merge strategy for concurrent updates (e.g., "append", "replace") |
@min(num) |
Exactly 1 | Minimum numeric value allowed for int or float variables |
@max(num) |
Exactly 1 | Maximum numeric value allowed for int or float variables |
Arguments are enclosed in parentheses and can be:
@description("User input")@min(0), @max(100), @default(0.5)@reducer(append)Multiple arguments are comma-separated. Trailing commas are allowed.
@required)@required takes 0 args)An agent block declares an execution unit — an LLM-powered step in the workflow.
agent <Identifier> {
instructions: <string> // Required
model: <string> // Optional
provider: <string> // Optional
temperature: <float> // Optional
tools: [<string>, ...] // Optional
inputs: [<identifier>, ...] // Optional
outputs: [<identifier>, ...] // Optional
}
| Property | Required | Type | Description |
|---|---|---|---|
instructions |
Yes | String | Prompt text for the agent. Cannot be empty. |
model |
No | String | LLM model identifier (e.g., "gemini-2.0-flash", "gpt-4"). Used directly — no mapping. |
provider |
No | String | LLM provider: "gemini" or "openai". Overrides auto-detection. |
temperature |
No | Float | Sampling temperature. Must be in range [0.0, 2.0]. |
tools |
No | String list | External tool names the agent can invoke. |
inputs |
No | Identifier list | State variables this agent reads. Must reference declared state variables. |
outputs |
No | Identifier list | State variables this agent writes. Must reference declared state variables. |
instructions is the only required propertymodel: etc.)inputs and outputs reference state variable names, not quoted stringsinputs or outputs are errorsmodel is set, the runtime requires OAF_DEFAULT_MODEL environment variableUse triple-quoted strings for multi-line content. Leading whitespace is automatically stripped (Python-style dedent):
agent Writer {
instructions: """
Write a clear, concise summary based on the key points.
Keep the tone professional.
Target length: 2-3 paragraphs.
"""
model: "gemini-2.0-flash"
temperature: 0.7
inputs: [key_points]
outputs: [summary]
}
By default, the runtime auto-detects the provider from API keys. You can override per agent:
agent AnalystGemini {
instructions: "Analyze data using Gemini."
model: "gemini-2.0-flash"
provider: "gemini"
inputs: [data]
outputs: [analysis]
}
agent WriterOpenAI {
instructions: "Write content using GPT-4."
model: "gpt-4"
provider: "openai"
inputs: [analysis]
outputs: [content]
}
| Temperature | Behavior | Use Case |
|---|---|---|
0.0 – 0.3 |
Deterministic, focused | Classification, extraction, analysis |
0.4 – 0.7 |
Balanced | General-purpose tasks |
0.8 – 1.2 |
Creative, varied | Writing, brainstorming |
1.3 – 2.0 |
Highly random | Experimental use |
The flow block defines the execution graph as directed edges between agents.
flow {
start -> AgentA
AgentA -> AgentB
AgentB -> end
}
<source> -> <destination>
Where <source> and <destination> are:
start (source only)end (destination only)graph LR
S((start)) --> A["AgentA"]
A --> B["AgentB"]
B --> E((end))
One agent can have multiple successors:
flow {
start -> Router
Router -> Analyst
Router -> Writer
Analyst -> end
Writer -> end
}
| Rule | Description |
|---|---|
| Exactly one start edge | There must be exactly one edge with start as the source |
| At least one end edge | At least one edge must target end |
| Reachability | All declared agents must be reachable from start |
| Termination | All declared agents must have a path to end |
| No duplicate edges | No two edges may have the same source and target |
| No self-loops | An edge cannot connect a node to itself |
| Acyclicity (v0.1) | The graph must be a DAG — no cycles |
No outgoing from end |
end cannot be a source |
No incoming to start |
start cannot be a target |
| Error Message | Cause |
|---|---|
Missing start edge |
No edge originates from start |
Multiple start edges |
More than one edge from start |
No edge leads to end |
No edge targets end |
Undefined agent in flow: "X" |
Edge references an undeclared agent |
Unreachable agent: "X" |
Agent cannot be reached from start |
Agent has no path to end: "X" |
Agent has no path to end |
Duplicate edge: A -> B |
Same edge declared twice |
Self-loop detected: A -> A |
Edge connects agent to itself |
Cycle detected in flow graph involving: A, B |
Circular dependency |
Outgoing edge from end node not allowed |
end used as a source |
Incoming edge to start node not allowed |
start used as a target |
The optional config block provides workflow-level metadata and runtime hints.
config {
<key>: <value>
...
}
config {
version: "0.1"
runtime: "langgraph"
max_iterations: 10
timeout_seconds: 300
}
Values can be strings, integers, floats, or booleans:
config {
version: "0.1" // string
max_iterations: 10 // integer
timeout_seconds: 300.5 // float
debug: true // boolean
}
| Key | Type | Validation |
|---|---|---|
max_iterations |
Positive integer | Must be > 0 |
timeout_seconds |
Positive number | Must be > 0 |
runtime |
String | Must be "langgraph" (currently the only supported runtime) |
Other keys are allowed but not validated — they pass through to the IR as-is.
config block per workflowmodel: "gpt-4"
instructions: "Say hello."
| Escape | Character |
|---|---|
\" |
Double quote |
\\ |
Backslash |
\n |
Newline |
\t |
Tab |
For multi-line content. Leading whitespace is automatically stripped using a common-indent algorithm (similar to Python’s textwrap.dedent):
instructions: """
Line one.
Line two.
Line three.
"""
// Result: "Line one.\nLine two.\nLine three."
The dedent algorithm:
Line comments begin with // and continue to the end of the line:
// This is a full-line comment
workflow "Test" { // This is an inline comment
agent A {
instructions: "Do something" // Comment after property
}
}
Comments are stripped during lexical analysis and do not appear in the AST.
Here’s a complete workflow using most language features:
// Customer feedback analysis pipeline
workflow "Customer Feedback Analysis" {
state {
feedback: string @required @description("Raw customer feedback text")
sentiment: string
category: string
key_issues: list[string]
response_draft: string
}
agent SentimentAnalyzer {
instructions: """
Analyze the customer feedback for sentiment.
Classify as: positive, negative, neutral, or mixed.
Return only the classification as a single word.
"""
model: "gpt-4"
temperature: 0.1
inputs: [feedback]
outputs: [sentiment]
}
agent Categorizer {
instructions: """
Based on the feedback and sentiment, categorize as:
bug_report, feature_request, praise, complaint, or question.
Also extract the top 3 key issues.
Return JSON with "category" and "key_issues" fields.
"""
model: "gpt-4"
temperature: 0.2
inputs: [feedback, sentiment]
outputs: [category, key_issues]
}
agent ResponseDrafter {
instructions: """
Draft a professional response addressing:
- The original feedback
- The detected sentiment
- The category
- The key issues
Keep it under 200 words.
"""
model: "gpt-4"
temperature: 0.7
inputs: [feedback, sentiment, category, key_issues]
outputs: [response_draft]
}
flow {
start -> SentimentAnalyzer
SentimentAnalyzer -> Categorizer
Categorizer -> ResponseDrafter
ResponseDrafter -> end
}
config {
version: "0.1"
runtime: "langgraph"
}
}