Observed since 21 Oct, 09:33 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
An agent flow engine extremely easy to define in YAML-style
Agy is a Python library for building, executing, validating and testing agent-based workflows with a compact flow language called FLOWSY.

agy init (recommended)# Install agy globally with uv
uv tool install agy
# Create a new project from template
agy init --template email_routing_mock
# or: agy init
# or: agy init --template software_support_jira
# or: agy init --template email_routing_gmail
# or: agy init --template email_routing_graph
# or: agy init --template email_routing_imap_smtp
# Setup and run
cd agy_email_routing_mock
uv sync
uv run python main.py
uv add agy
Or with pip:
pip install agy
The first public release keeps Email and Jira integrations in the core package, so the templates work without extra dependency groups:
uv sync
For a complete list of required and optional environment variables (LLM providers, Jira, Graph/Gmail/IMAP-SMTP email, and safety settings), see:
For runnable repository examples, use:
cp .env.example .env
Then set values required by the examples you run (JIRA_URL, JIRA_TOKEN,
JIRA_ISSUE_KEY, JIRA_JQL, OPENAI_API_KEY).
More on the project structure: See docs/agy/CLI_PROJECT_STRUCTURE.md
If an existing project installed Agy from a private Git URL, switch to the public package:
[project]
dependencies = ["agy>=1.0.0"]
The Python import path is unchanged (import agy). See
docs/AGY_VS_AGY_PRIVATE.md for a full migration
guide from agy-private 0.8.1.
Create a simple email routing flow that classifies emails and routes them to the appropriate department.
1. Define the flow (email_routing_flow.flowsy):
name: Email Routing Flow
description: Classify and route customer emails
context_in:
email: Email
nodes:
classify_email:
actions:
- category = classify(input_text=email.text, categories=["sales", "support", "billing"], instruction_file="prompts/email_classify_instruction.md")
edges:
- confidence < 0.8: handle_unclear
- category == "sales": handle_sales
- category == "support": handle_support
- category == "billing": handle_billing
handle_sales:
actions:
- email.forward("sales@example.com")
- email.move("processed")
handle_support:
actions:
- answer = respond(input_text=email.text, instruction="Provide helpful support answer")
- email.reply(answer)
- email.move("processed")
handle_billing:
actions:
- answer = respond(input_text=email.text, instruction="Handle billing inquiry professionally")
- email.reply(answer)
- email.move("processed")
handle_unclear:
actions:
- email.move("human_team")
2. Run the flow (main.py):
import asyncio
from agy import Flow
from objects.email import Email
async def main():
# Create email object
email = Email(
sender="customer@example.com",
recipient="support@company.com",
subject="Urgent",
text="I need help with my invoice #12345. I was charged twice.",
)
# Validate the flow once against the expected context classes (optional)
validation_result = Flow.validate("email_routing_flow.flowsy", context_in={"email": Email})
if not validation_result.is_valid:
print(f"Validation failed: {len(validation_result.errors)} errors")
flow = Flow.from_flowsy("email_routing_flow.flowsy")
# Execute flow with concrete object instances
result = await flow.run(context_in={"email": email})
print(f"Category: {result.get('result')}")
print(f"Confidence: {result.get('confidence')}")
asyncio.run(main())
Try it yourself:
# if you have not done it:
uv run agy init --template email_routing_mock
cd agy_email_routing_mock
# make sure, that you have your personal model key of the model you want to use in your ENV
# Default model in the pyproject.toml is a (small) OPENAI model, please change, if your favorite model is a different one
export OPENAI_API_KEY="sk-........................"
python main.py
See docs/integrations/EMAIL.md and docs/agy/FLOW.md for complete end-to-end
examples.
Use context_in to inject domain objects (for example Email, JiraClient)
and call their methods directly in flow actions.
📖 Complete Parsing Reference: For detailed information about the
.flowsyfile format, indentation rules, expression syntax, and parsing details, see docs/agy/FLOW_PARSING.MD.
A flow consists of:
name, description)context_in)actions)end(...))name: My Flow
description: Brief description of what this flow does
context_in:
variable_name_1: TypeName2
variable_name_2: TypeName2
context_in: Input parameters passed to the flow
Nodes are the building blocks of your flow. Each node has:
nodes:
node_name:
actions:
- action1()
- result = action2(param=value)
edges:
- condition: target_node
- True: fallback_node
Stochastic nodes delegate one or more natural-language requests to an agent
object from context_in. The agent must expose a callable run(...)
method. Agy normalizes the agent result and writes the final output, message,
all intermediate outputs, success, error_msg and result into the flow
context before evaluating edges.
context_in:
consc: Consciousness
project_name: str
nodes:
summarize_yesterday:
type: stochastic
agent: consc
requests:
- f'Find all mails discussing {project_name} in all different spellings in the mails of the last two weeks.'
- "Create a Markdown table with content, sender and urgency."
- "Add an answered column showing whether I answered the topic in substance."
output: email_report_md
message: agent_message
edges:
- success == True: send_report
- True: end(success=False, error_msg=error_msg)
send_report:
actions:
- df = format_md_to_pd(email_report_md)
- filename = save_df_to_excel(df)
Agent return values are normalized to an AgentRequestResult shape with
outputs, output, message, success, error_msg and raw. Returning a
plain string is also supported and is treated as a successful output.
Each requests: entry may be plain text or a Python expression evaluated with
the current flow context before calling agent.run(...). Evaluated request
expressions must return a string.
Actions are operations executed within a node:
actions:
# Simple action call
- show("Hello, here I am")
# Action with result assignment
- result = classify(input_text=text, categories=["legal", "business", "scientific"])
# Object method calls
- database.save(record)
- api.send_request(data)
*Built-in actions, LLM:**classify(), respond(), extract()*IO:
load_files_text(), show() FLow Control: end() Details: see below
Edges define the flow routing from one node to another:
edges:
# Conditional edge (evaluated in order)
- confidence < 0.8: low_confidence_handler
- category == "urgent": urgent_handler
# Unconditional edge (fallback)
- True: default_handler
# Implicit termination of the flow
# finalizing the last action of a node WITHOUT edges => successful termination of the flow
Actions are the executable operations within flow nodes. They are processed
sequentially by the ActionExecutor and can modify the flow's context.
success=False in context):
end() action immediately terminates the flow with
custom context valuesExample:
nodes:
example_node:
actions:
- result1 = action1() # Executes first
- result2 = action2() # Executes only if action1 succeeded
- action3() # Executes only if action1 & action2 succeeded
Two types of actions:
Global Functions (global_function actions)
ActionRegistryclassify(), respond(), show()flow.run(action_types=[...])Object Methods
context_inobject_name.method_name(args)email.reply("Thanks"), document.save()All LLM actions return a dictionary with their result and a confidence value
(0.0-1.0) that is automatically written to the context for flow routing
decisions.
classify() - Classifies text based on generic classification prompt,
custom categories and optional instructions/augmentation
nodes:
- classify_document:
actions:
- category = classify(input_text=document.text, categories=["legal",
"business", "financial"], instruction_file="cat_guide_acme.md")
# Returns: category="legal", confidence=0.85 (both in context)
edges:
- confidence < 0.7: manual_review
- category == "legal": process_legal_doc
- category == "business": process_business_doc
- category == "financial": process_financial_doc
respond() - Generates text using generic respond prompt, optional
instructions and augmentation context
- answer = respond(input_text=query, instruction_file="prompts/response.md", augmentation=search_results)
# Returns: answer="Generated text...", confidence=0.92 (both in context)
extract() - Extracts structured data using generic extract prompt and
schema definition
- invoice = extract(input_text=invoice_pdf_text, values_to_extract={"invoice_number": "str", "total": "float", "date": "str"})
# Returns: invoice={"invoice_number": "INV-123", "total": 1500.50, "date": "2025-01-15"}, confidence=0.88
Note: All LLM actions return their result and write confidence (0.0-1.0)
to context for easy access in edges.
set_model_call(provider, model, **kwargs)** - Configure LLM
provider/model for all subsequent LLM actions (OpenAI, Azure OpenAI, Gemini,
Anthropic, or custom)model_call(prompt, **kwargs)** - Direct LLM call with custom prompt
(uses configured provider/model)get_prompt_from_str(template, **kwargs) / get_prompt_from_file(file_path, **kwargs) - Format
prompts with variablesMulti-Provider Support:
pyproject.toml [tool.agy.llm]set_model_call(provider="anthropic", model="claude-haiku-4-5-20251001")run_flow() - Call another flow (or re-enter the current flow at a
specific node) and return its result:
# Call an external sub-flow
- sub = run_flow(flow="detail_check.flowsy", ticket=ticket, jira=jira)
# Jump to a node in the current flow
- sub = run_flow(node="classify", email=email)
run_flow_batch() - Run a sub-flow once per list element (sequential or
parallel):
- results = run_flow_batch(emails, element="email", flow="process.flowsy", mode="parallel")
Each iteration gets its own isolated context. Results are returned as a list.
Supports on_error="continue" (default) or on_error="fail_fast".
search_emails() - Search an email account and return a RecordSet with
readable batch methods. Existing single-email flows still receive regular
Email objects; RecordSet is only used when a flow explicitly searches a
batch.
context_in:
account: EmailAccount
consc: Consciousness
nodes:
retrieve_emails:
actions:
- retrieved_emails = search_emails(account, query="customs transfer", folders=["inbox"], batch_size=10)
- current_email_batch = retrieved_emails.get_next_batch()
edges:
- screen_batch
screen_batch:
type: stochastic
agent: consc
requests:
- "Review the current email batch and return the ids of emails that mention customs transfers."
output: candidate_email_ids
message: screening_message
edges:
- len(candidate_email_ids) > 0: load_candidate_emails
- retrieved_emails.has_next_batch(): next_email_batch
- finalize_not_found
next_email_batch:
actions:
- current_email_batch = retrieved_emails.get_next_batch()
edges:
- screen_batch
load_candidate_emails:
actions:
- candidate_emails = retrieved_emails.load_full(candidate_email_ids)
edges:
- deep_review
Use domain-specific variable names in flows, such as retrieved_emails,
current_email_batch and candidate_emails. Technically these are RecordSet
or Record values, but the flow should read like business logic.
search_files() - Search local files and return a RecordSet with the
same readable batch methods. File records start as metadata-only records;
load_full(ids) loads text only for selected candidates via Agy's file parsers.
context_in:
root_path: str
consc: Consciousness
nodes:
retrieve_files:
actions:
- retrieved_files = search_files(root_path=root_path, query="customs", file_extensions=["pdf", "docx", "md"], batch_size=10)
- current_file_batch = retrieved_files.get_next_batch()
edges:
- screen_file_batch
screen_file_batch:
type: stochastic
agent: consc
requests:
- "Review the current file batch and return the ids of files that mention customs transfers."
output: candidate_file_ids
message: file_screening_message
edges:
- len(candidate_file_ids) > 0: load_candidate_files
- retrieved_files.has_next_batch(): next_file_batch
- finalize_not_found
load_candidate_files:
actions:
- candidate_files = retrieved_files.load_full(candidate_file_ids)
edges:
- deep_review
load_files_text() - Load and parse files (PDF, DOCX, XLSX, TXT, HTML)show() - Print debug informationend() - Explicitly terminate flow with custom context valuesFull reference: See docs/agy/ACTIONS_REFERENCE.md
Define custom actions using ActionType and pass them to flow.run(...):
from agy import ActionType, Flow
def calculate_score(value: int, multiplier: int = 2) -> int:
"""Calculate a score by multiplying value with multiplier."""
return value * multiplier
score_action = ActionType(
object_name="global_function",
method_name="calculate_score",
kwargs={"value": int, "multiplier": int},
callable=calculate_score
)
flow = Flow.from_flowsy("my_flow.flowsy")
result = await flow.run(action_types=[score_action], context_in={})
Full guide: See docs/agy/ACTIONS_REFERENCE.md
Flows receive input data through context_in when executed:
from agy import Flow
from objects.email import Email
# Create flow
flow = Flow.from_flowsy("email_routing_flow.flowsy")
# Create context objects
email = Email(text="...", From="user@example.com")
# Execute with context_in
result = await flow.run(context_in={"email": email})
Flow file declares required inputs:
context_in:
email: Email
settings: dict
flow.run(...) validates that all declared keys are provided at runtime.
The flow maintains a context dictionary that:
context_in objectsStandard context keys (automatically managed):
success (bool): Indicates if the last action succeedederror_msg (str): Error message if action failedconfidence (float): Confidence value from LLM actionscategory (str): Category from classify()result (any): Default output variable for actions: Every function call,
method call wit return value not None and without defined output variable
writes to result:actions:
- add (0.2, 0.52)
# result = 0.72
Variable scope:
context_in are available everywhereanswer = respond(...)) are available
in:
Accessing context data in actions:
actions:
# Access context_in object
- show(email.Subject)
# Access object attributes
- classify(input_text=email.text, ...)
# Access assigned variables
- respond(input_text=question, augmentation=search_results)
# Access dict keys
- value = settings["api_key"]
After execution, flow.run() returns the final context dictionary:
context = await flow.run(context_in={"email": email})
# Access results
print(context["success"]) # True/False
print(context["category"]) # From classify()
print(context["confidence"]) # From LLM actions
print(context["error_msg"]) # Error message if any
print(context["custom_value"]) # Any value set via end(custom_value=...) or as an action "- custom_value = action_x ()"
Edges define conditional routing between nodes. They are evaluated sequentially until a match is found.
Conditional Edge:
edges:
- confidence < 0.7: manual_review
- category == "urgent": urgent_handler
Unconditional Edge (Fallback):
edges:
- True: default_handler # Always matches if reached
As a friendlier shorthand, the final edge may omit the condition entirely:
edges:
- confidence < 0.7: manual_review
- default_handler # Same as `True: default_handler`; only valid as the last edge
No Edges (Implicit Termination):
nodes:
final_step:
actions:
- email.reply("Done!")
# No edges = flow ends here
<, >, <=, >=, ==, !=and, or, notin, not inExamples:
edges:
# Numeric comparison
- confidence < 0.7: low_confidence
# String equality
- category == "urgent": urgent_handler
- status == "approved": process_approval
# Boolean checks
- is_valid and not requires_review: auto_approve
# Multiple conditions
- confidence >= 0.8 and category == "sales": high_confidence_sales
# Fallback (always true)
- True: default_handler
Implicit Termination:
success is True if all actions succeeded, False if any failedExplicit Termination:
- end(success=False, error_msg="Validation failed")
Edge to end:
edges:
- is_complete: end(success=True) # Terminates flow
- continue_processing
To create a new release (bump version, tag, and publish GitHub Release with build artifacts), follow the steps in docs/dev/RELEASE_STEPS.md.
Download counts and popularity will appear here as the collector receives them.
| Version | Published | Direct dependencies | Open version |
|---|---|---|---|
| 1.2.0 | 30 Sept 2026 | 27 |
This describes our collected data; it is not a security assessment.
Observed since 21 Oct, 09:33 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package:c283966b76118ac45e02b1a454189d80f596d7212bce2eb6ae4f624004abed3b