Skip to main content

What Are Steps?

Steps = Focused ResponsibilitiesEach step has a single, well-defined purpose in your agent’s workflow. This decomposition makes complex agent behavior manageable and predictable.

Single Purpose

Each step handles one specific type of interaction or task

Controlled Tools

Steps only have access to tools they actually need

Clear Transitions

Explicit conditions determine when and where to move next

Independent Testing

Each step can be tested and validated separately

Step Anatomy

Basic Step Structure

steps:
  - step_id: greet_customer  # Can also use 'id'
    description: |            # Can also use 'desc'
      Welcome the customer and understand their needs.
      Ask how you can help them today and listen for their response.
    available_tools: []       # Can also use 'tools'
    routes:                   # Can also use 'paths'
      - target: take_order    # Can also use 'to'
        condition: Customer wants to place an order  # Can also use 'when'
      - target: customer_support
        condition: Customer has questions or needs help
      - target: end
        condition: Customer wants to leave
    examples:                 # Can also use 'eg'
      - context: "New customer enters"
        decision: "Greet warmly and ask how to help"
        visibility: "always"
Field Naming FlexibilityNOMOS supports both compact (id, desc, tools, paths, to, when, eg) and descriptive (step_id, description, available_tools, routes, target, condition, examples) field names. You can use either convention or mix them in the same configuration file.

Step Components

  • Identity
  • Behavior
  • Capabilities
  • Flow Control
Step ID: Unique identifier for the step
step_id: take_coffee_order  # or 'id'
This is how other steps reference this step in their routes.

Advanced Step Features

Step Examples

Provide examples to help the LLM understand expected behavior:
- step_id: handle_complaint
  description: |
    Listen to customer complaints with empathy and work toward resolution.
  available_tools:
    - lookup_order_history
    - check_return_policy
    - issue_refund
  examples:
    - context: "Customer received damaged item"
      decision: "Apologize, lookup order, offer refund or replacement"
      visibility: "always"
    - context: "Customer angry about delivery delay"
      decision: "Acknowledge frustration, investigate delay, offer compensation"
      visibility: "when_relevant"

Conditional Tool Access

Tools can be conditionally available based on context:
- step_id: manager_escalation
  description: |
    Handle escalated issues that require manager authority.
  available_tools:
    - basic_customer_lookup
    - issue_special_discount  # Only available in this step
    - override_policy        # Manager-level tool
    - escalate_to_regional   # Final escalation option
  routes:
    - target: resolution_complete
      condition: Issue resolved with manager intervention
    - target: regional_escalation
      condition: Issue requires regional manager

Overrides

Steps can override certain configurations for optimal usage:
- step_id: code_generation
  description: |
    Generate code
  overrides:
    persona: You are a coding assistant.
    llm: coding

Answer Models and Structured Responses

What Are Answer Models?

Answer models define the expected structure and format of your agent’s responses. They ensure consistent, parseable output that can be easily processed by downstream systems or validated for correctness.
Why Use Answer Models?Answer models provide type safety, validation, and structured data extraction from your agent’s responses. They’re especially valuable for:
  • API integrations
  • Data processing workflows
  • Multi-step processes requiring consistent data formats
  • Quality assurance and testing

Inline Answer Models

Define simple answer models directly in your step configuration:
- step_id: collect_user_info
  description: |
    Ask for user's name and email address.
  answer_model:
    name:
      type: str
      description: User's full name
    email:
      type: str
      description: User's email address
    age:
      type: int
      description: User's age in years
      optional: true

External Schema Files

For complex or reusable models, define schemas in separate files:
# config.agent.yaml
name: my_agent

schemas:
  user_profile: 'schemas/user_profile.json'
  order_schema: 'schemas/order.py'

steps:
  - step_id: collect_profile
    description: Collect user profile information
    answer_model: user_profile  # References external schema

  - step_id: process_order
    description: Process customer order
    answer_model: order_schema.Order  # Specific model from Python file

JSON Schema Format

Create schemas/user_profile.json:
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "description": "User's full name"
    },
    "email": {
      "type": "string",
      "description": "User's email address",
      "format": "email"
    },
    "age": {
      "type": "integer",
      "description": "User's age in years",
      "minimum": 0,
      "maximum": 150
    },
    "preferences": {
      "type": "object",
      "properties": {
        "theme": {
          "type": "string",
          "enum": ["light", "dark", "auto"]
        },
        "notifications": {
          "type": "boolean"
        }
      }
    }
  },
  "required": ["name", "email"]
}

Python Schema Format

Create schemas/order.py:
from pydantic import BaseModel, EmailStr
from typing import List, Optional
from enum import Enum

class OrderStatus(str, Enum):
    PENDING = "pending"
    CONFIRMED = "confirmed"
    PROCESSING = "processing"
    SHIPPED = "shipped"
    DELIVERED = "delivered"

class OrderItem(BaseModel):
    product_id: str
    name: str
    quantity: int
    price: float
    description: Optional[str] = None

class ShippingAddress(BaseModel):
    street: str
    city: str
    state: str
    zip_code: str
    country: str = "US"

class Order(BaseModel):
    order_id: str
    customer_email: EmailStr
    items: List[OrderItem]
    shipping_address: ShippingAddress
    status: OrderStatus = OrderStatus.PENDING
    total_amount: float
    created_at: str
    notes: Optional[str] = None

    def calculate_total(self) -> float:
        """Calculate total from items"""
        return sum(item.price * item.quantity for item in self.items)

Schema Referencing

Reference schemas in different ways:
# Reference main model from schema
answer_model: user_profile

# Reference specific model from Python schema
answer_model: order_schema.Order

# Reference nested model
answer_model: order_schema.OrderItem

Benefits of External Schemas

Reusability

Use the same schema across multiple agents and steps

Maintainability

Update schemas without touching agent configuration

Type Safety

Full Pydantic validation with rich error messages

Version Control

Track schema changes separately from agent logic

IDE Support

Better autocomplete and validation in Python schemas

Documentation

Self-documenting schemas with descriptions and examples

Advanced Schema Features

Nested Models and References

# schemas/complex_order.py
from pydantic import BaseModel
from typing import List
from .user_profile import UserProfile  # Reference other schemas

class OrderSummary(BaseModel):
    order_id: str
    customer: UserProfile  # Nested reference
    items_count: int
    total: float

class OrderDetails(OrderSummary):
    items: List[OrderItem]
    shipping_info: ShippingAddress
    tracking_number: Optional[str] = None

Validation and Constraints

# schemas/validated_user.py
from pydantic import BaseModel, Field, validator
from typing import Optional

class ValidatedUser(BaseModel):
    username: str = Field(..., min_length=3, max_length=50)
    email: str = Field(..., regex=r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
    age: Optional[int] = Field(None, ge=0, le=150)

    @validator('username')
    def username_alphanumeric(cls, v):
        if not v.replace('_', '').replace('-', '').isalnum():
            raise ValueError('Username must be alphanumeric with underscores or hyphens')
        return v

Best Practices for Answer Models

1. Start Simple

Progressive Enhancement

Begin with basic models and add complexity as needed
# Start simple
answer_model:
  result: str
  success: bool

# Add complexity later
answer_model:
  result: str
  success: bool
  data:
    type: object
    properties:
      items: {type: array}
      metadata: {type: object}

2. Use Descriptive Field Names

Self-Documenting

Field names should clearly indicate their purpose
# Good: Descriptive names
answer_model:
  customer_full_name: str
  order_total_amount: float
  estimated_delivery_date: str

# Avoid: Generic names
# answer_model:
#   name: str
#   total: float
#   date: str

3. Include Validation Rules

Data Quality

Add constraints to ensure data quality
answer_model:
  email:
    type: str
    description: Valid email address
    pattern: '^[^\s@]+@[^\s@]+\.[^\s@]+$'
  age:
    type: int
    description: Age in years
    minimum: 0
    maximum: 150

4. Document Complex Schemas

Future Maintenance

Add descriptions for complex or non-obvious fields
answer_model:
  processing_status:
    type: str
    description: |
      Current status of the order processing pipeline.
      Values: 'received', 'validated', 'processing', 'shipped', 'delivered'
    enum: ['received', 'validated', 'processing', 'shipped', 'delivered']

Step Transitions and Routing

Route Conditions

Route conditions should be clear and specific:
routes:
  # Good: Specific conditions
  - target: payment_processing
    condition: Customer provided valid payment method and confirmed order total

  # Good: Business logic conditions
  - target: manager_approval
    condition: Order total exceeds $1000 and requires manager approval

  # Good: Error handling
  - target: retry_payment
    condition: Payment failed due to insufficient funds

  # Avoid: Vague conditions
  # - target: next_step
  #   condition: When ready

Complex Routing Logic

Handle multiple routing scenarios:
- step_id: order_validation
  description: |
    Validate order details and route based on validation results.
  available_tools:
    - validate_shipping_address
    - check_inventory_availability
    - verify_payment_method
  routes:
    - target: process_order
      condition: All validations pass and order can be processed immediately
    - target: address_correction
      condition: Shipping address needs correction
    - target: inventory_backorder
      condition: Some items are backordered but customer accepts delay
    - target: payment_issues
      condition: Payment method validation failed
    - target: order_cancellation
      condition: Critical validation failures that cannot be resolved

Error Handling in Steps

Graceful Error Recovery (Coming Soon)

- step_id: external_api_integration
  description: |
    Integrate with external service, handling potential failures gracefully.
  available_tools:
    - call_external_api
    - fallback_local_data
    - notify_technical_team
  routes:
    - target: api_success_processing
      condition: External API call successful
    - target: fallback_processing
      condition: API unavailable but fallback data sufficient
    - target: service_unavailable
      condition: Neither API nor fallback available
  error_handling:
    max_retries: 3
    timeout: 30
    fallback_step: service_unavailable

Step Performance and Optimization

Efficient Tool Usage

- step_id: optimized_lookup
  description: |
    Efficiently lookup customer information using cached data when possible.
  available_tools:
    - quick_customer_lookup   # Fast, cached lookup
    - detailed_customer_fetch # Slower, comprehensive lookup
    - update_customer_cache   # Update cache with new info
  routes:
    - target: serve_from_cache
      condition: Customer info found in cache and is recent
    - target: fetch_and_cache
      condition: Customer info not cached or outdated

Testing Individual Steps

Steps can be tested independently:
# tests.agent.yaml
unit:
  test_greeting_step:
    context:
      current_step_id: "greeting"
    input: "Hello, I'm looking for help with my order"
    expectation: "Greets customer and routes to support_request step"

  test_order_step_with_tools:
    context:
      current_step_id: "take_order"
    input: "I'd like a large latte with oat milk"
    expectation: "Uses menu tools and adds specific item to cart"

  test_error_handling:
    context:
      current_step_id: "payment_processing"
    input: "My card was declined"
    expectation: "Provides helpful error message and routes to payment_retry"

Best Practices for Step Design

1. Single Responsibility Principle

One Job Per Step

Each step should have one clear purpose
# Good: Focused steps
- step_id: collect_shipping_address
- step_id: validate_shipping_address
- step_id: calculate_shipping_cost

# Avoid: Overloaded steps
# - step_id: handle_all_shipping_stuff

2. Clear Transition Logic

Explicit Routing

Make transition conditions obvious and testable
routes:
  # Good: Clear conditions
  - target: payment_success
    condition: Payment processed successfully and confirmation email sent

  # Avoid: Ambiguous conditions
  # - target: next
  #   condition: When done

3. Appropriate Tool Access

Minimal Tool Scope

Only provide tools that are needed for the step’s purpose
- step_id: customer_greeting
  available_tools: []  # No tools needed for greeting

- step_id: lookup_order
  available_tools:
    - search_orders    # Only order search capability

- step_id: process_refund
  available_tools:
    - search_orders    # Need to find order
    - issue_refund     # Need to process refund
    # No other payment tools

4. Comprehensive Error Handling

Expect the Unexpected

Plan for failure scenarios and edge cases
- step_id: api_dependent_step
  routes:
    - target: success_path
      condition: API call successful
    - target: retry_path
      condition: API call failed but retryable
    - target: fallback_path
      condition: API unavailable but alternative exists
    - target: failure_path
      condition: No alternatives available
Start Simple, Then RefineBegin with basic steps and gradually add complexity. It’s easier to split a step that’s doing too much than to combine steps that are too granular.

Real-World Step Examples

Here are well-designed steps from the NOMOS examples:
- step_id: start
  description: |
    Greet the customer and ask how can I help them.
    Use the get_available_coffee_options tool if they need menu information.
  available_tools:
    - get_available_coffee_options
  routes:
    - target: take_coffee_order
      condition: Customer is ready to place a new order

- step_id: take_coffee_order
  description: |
    Ask for coffee preference and size.
    Manage cart operations based on customer requests.
  available_tools:
    - get_available_coffee_options
    - add_to_cart
    - remove_item
    - clear_cart
  routes:
    - target: finalize_order
      condition: User wants to finalize the order
    - target: end
      condition: Customer wants to cancel
Steps are the foundation of reliable AI agents. By designing them with clear purposes, appropriate tool access, and explicit transition logic, you create agents that are both powerful and predictable.