Motia Icon

Agentic Workflows

Learn how to build intelligent agentic workflows that make decisions and automate workflows with Motia

What You'll Build

An intelligent pet management system with agentic workflows that automate decisions and enrich data:

  • AI Profile Enrichment - Automatically generates detailed pet profiles using AI
  • Health Review Agentic Step - Makes intelligent health decisions based on symptoms
  • Adoption Review Agentic Step - Assesses adoption readiness and data completeness
  • Orchestrator Integration - AI decisions that drive real workflow changes

workbench

Getting Started

Clone the example repository:

git clone https://github.com/MotiaDev/build-your-first-app.git
cd build-your-first-app
git checkout ai-agents

Install dependencies:

npm install

Set up your OpenAI API key in .env:

OPENAI_API_KEY=your_api_key_here

Important! This tutorial requires an OpenAI API key. Get yours at platform.openai.com/api-keys. Without it, the agentic workflows won't work.

Start the Workbench:

npm run dev

Your Workbench will be available at http://localhost:3000.


Project Structure

.env
package.json
requirements.txt
types.d.ts

Files like features.json and tutorial/tutorial.tsx are only for the interactive tutorial and are not part of Motia's project structure.

All code examples in this guide are available in the build-your-first-app repository.

You can follow this guide to learn how to build agentic workflows with Motia step by step, or you can clone the repository and dive into our Interactive Tutorial to learn by doing directly in the Workbench.

interactive-tutorial


Understanding Agentic Workflows

You've built APIs, background jobs, and workflows that orchestrate your pet shelter. But what about decisions that aren't black and white? Should this pet's symptoms require treatment? Is this profile ready for the adoption page?

That's where agentic workflows come in. They're smart assistants that make judgment calls based on context - the kind of decisions that would normally need a human to review every single case.

In our pet shelter, we use two flavors:

  • Content generators write engaging pet profiles automatically
  • Decision makers evaluate health symptoms and choose whether treatment is needed
  • Data reviewers assess if adoption information is complete

The difference from traditional code? Instead of writing hundreds of if-else rules for every possible symptom combination, you describe what matters to the AI. It reads the context and makes an informed call.

When a pet arrives with "coughing, lethargy, loss of appetite" - the AI evaluates these symptoms together and decides if treatment is needed. No hardcoded rules. Just intelligent analysis of the situation.


Creating Your First Agentic Step

Let's start with a content generation agentic step that automatically enriches pet profiles when they're created.

Step 1: Set Up Pet Creation to Emit Events

First, update your pet creation endpoint to emit events that will trigger the agentic step.

View on GitHub:

steps/typescript/create-pet.step.ts
import { ApiRouteConfig, Handlers } from 'motia'
import { z } from 'zod'
import { TSStore } from './ts-store'
 
const createPetSchema = z.object({
  name: z.string().min(1, 'Name is required').trim(),
  species: z.enum(['dog', 'cat', 'bird', 'other']),
  ageMonths: z.number().int().min(0, 'Age must be a positive number')
})
 
export const config: ApiRouteConfig = {
  type: 'api',
  name: 'TsCreatePet',
  path: '/ts/pets',
  method: 'POST',
  emits: ['ts.pet.created', 'ts.feeding.reminder.enqueued'],
  flows: ['TsPetManagement']
}
 
export const handler: Handlers['TsCreatePet'] = async (req, { emit, logger }) => {
  try {
    const validatedData = createPetSchema.parse(req.body)
    
    const pet = TSStore.create({ 
      name: validatedData.name, 
      species: validatedData.species, 
      ageMonths: validatedData.ageMonths 
    })
    
    if (logger) {
      logger.info('🐾 Pet created', { petId: pet.id, name: pet.name, species: pet.species, status: pet.status })
    }
    
    if (emit) {
      await (emit as any)({
        topic: 'ts.pet.created',
        data: { petId: pet.id, event: 'pet.created', name: pet.name, species: validatedData.species }
      })
      
      await (emit as any)({
        topic: 'ts.feeding.reminder.enqueued',
        data: { petId: pet.id, enqueuedAt: Date.now() }
      })
    }
 
  return { status: 201, body: pet }
    
  } catch (error) {
    if (error instanceof z.ZodError) {
      return {
        status: 400,
        body: {
          message: 'Validation error',
          errors: error.errors
        }
      }
    }
    
    return {
      status: 500,
      body: { message: 'Internal server error' }
    }
  }
}

The API endpoint now emits an event after creating a pet. The response returns immediately while the agentic step processes asynchronously in the background.


Step 2: Create the AI Profile Enrichment Agentic Step

Now let's create the agentic step that listens for new pets and enriches their profiles with AI-generated content.

View on GitHub:

steps/typescript/ai-profile-enrichment.step.ts
// steps/typescript/ai-profile-enrichment.step.ts
import { EventConfig, Handlers } from 'motia';
import { TSStore, PetProfile } from './ts-store';
 
export const config = {
  type: 'event',
  name: 'TsAiProfileEnrichment',
  description: 'Agentic step that enriches pet profiles using OpenAI',
  subscribes: ['ts.pet.created'],
  emits: [],
  flows: ['TsPetManagement']
};
 
export const handler: Handlers['TsAiProfileEnrichment'] = async (input, { logger }) => {
  const { petId, name, species } = input;
 
  if (logger) {
    logger.info('🤖 AI Profile Enrichment started', { petId, name, species });
  }
 
  try {
    const apiKey = process.env.OPENAI_API_KEY;
    if (!apiKey) {
      throw new Error('OPENAI_API_KEY environment variable is not set');
    }
 
    const prompt = `Generate a pet profile for adoption purposes. Pet details:
- Name: ${name}
- Species: ${species}
 
Please provide a JSON response with these fields:
- bio: A warm, engaging 2-3 sentence description that would appeal to potential adopters
- breedGuess: Your best guess at the breed or breed mix (be specific but realistic)
- temperamentTags: An array of 3-5 personality traits (e.g., "friendly", "energetic", "calm")
- adopterHints: Practical advice for potential adopters (family type, living situation, care needs)
 
Keep it positive, realistic, and adoption-focused.`;
 
    const response = await fetch('https://api.openai.com/v1/chat/completions', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${apiKey}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        model: 'gpt-3.5-turbo',
        messages: [
          {
            role: 'system',
            content: 'You are a pet adoption specialist who creates compelling, accurate pet profiles. Always respond with valid JSON only.'
          },
          {
            role: 'user',
            content: prompt
          }
        ],
        max_tokens: 500,
        temperature: 0.7,
      }),
    });
 
    if (!response.ok) {
      throw new Error(`OpenAI API error: ${response.status} ${response.statusText}`);
    }
 
    const data = await response.json();
    const aiResponse = data.choices[0]?.message?.content;
 
    if (!aiResponse) {
      throw new Error('No response from OpenAI API');
    }
 
    let profile: PetProfile;
    try {
      profile = JSON.parse(aiResponse);
    } catch (parseError) {
      profile = {
        bio: `${name} is a wonderful ${species} looking for a loving home. This pet has a unique personality and would make a great companion.`,
        breedGuess: species === 'dog' ? 'Mixed Breed' : species === 'cat' ? 'Domestic Shorthair' : 'Mixed Breed',
        temperamentTags: ['friendly', 'loving', 'loyal'],
        adopterHints: `${name} would do well in a caring home with patience and love.`
      };
      
      if (logger) {
        logger.warn('⚠️ AI response parsing failed, using fallback profile', { petId, parseError: parseError instanceof Error ? parseError.message : String(parseError) });
      }
    }
 
    const updatedPet = TSStore.updateProfile(petId, profile);
    
    if (!updatedPet) {
      throw new Error(`Pet not found: ${petId}`);
    }
 
    if (logger) {
      logger.info('✅ AI Profile Enrichment completed', { 
        petId, 
        profile: {
          bio: profile.bio.substring(0, 50) + '...',
          breedGuess: profile.breedGuess,
          temperamentTags: profile.temperamentTags,
          adopterHints: profile.adopterHints.substring(0, 50) + '...'
        }
      });
    }
 
  } catch (error: any) {
    if (logger) {
      logger.error('❌ AI Profile Enrichment failed', { 
        petId, 
        error: error.message 
      });
    }
 
    const fallbackProfile: PetProfile = {
      bio: `${name} is a lovely ${species} with a unique personality, ready to find their forever home.`,
      breedGuess: species === 'dog' ? 'Mixed Breed' : species === 'cat' ? 'Domestic Shorthair' : 'Mixed Breed',
      temperamentTags: ['friendly', 'adaptable'],
      adopterHints: `${name} is looking for a patient and loving family.`
    };
 
    TSStore.updateProfile(petId, fallbackProfile);
  }
};

How This Agentic Step Works

This is a content generation agentic step - it enriches data without making workflow decisions:

  • Subscribes to pet.created events
  • Calls OpenAI with a carefully crafted prompt
  • Parses the response into structured data
  • Updates the pet with AI-generated content
  • Has a fallback if the AI call fails

The key is the prompt engineering - we tell the AI exactly what fields we need and what tone to use. The AI returns JSON that we can parse and store directly.


Testing Your Agentic Step

The best way to test your agentic step is through Workbench. It lets you create pets, watch the AI enrichment happen in real-time, and see all the logs in one place.

Create a Pet

Open Workbench and test the CreatePet endpoint. The AI will automatically start enriching the profile in the background.

Prefer using curl?

curl -X POST http://localhost:3000/ts/pets \
  -H "Content-Type: application/json" \
  -d '{"name": "Max", "species": "dog", "ageMonths": 24}'

Check the logs in Workbench to see the agentic step in action:

ai-enrichment-logs

You'll see:

  1. "Pet created" log from the API endpoint
  2. "AI Profile Enrichment started" log
  3. "AI Profile Enrichment completed" with generated content

View the Enriched Profile

Fetch the pet in Workbench to see the AI-generated profile, or use curl:

Using curl?

curl http://localhost:3000/ts/pets/1

You'll get back something like:

ai-enrichment-logs


Building a Decision-Making Agentic Step

Now let's create an agentic step that doesn't just generate content - it makes decisions that control the workflow. This is called agentic routing.

The Health Review Agentic Step

This agentic step analyzes pet symptoms and decides if treatment is needed. Instead of you writing complex if-else logic, the AI evaluates the context and chooses the appropriate action.

View on GitHub:

steps/typescript/health-review-agent.step.ts
// steps/typescript/health-review-agent.step.ts
import { ApiRouteConfig, Handlers } from 'motia';
import { TSStore } from './ts-store';
import { 
  HEALTH_REVIEW_EMITS, 
  buildAgentContext, 
  callAgentDecision,
  getAgentArtifacts
} from './agent-decision-framework';
 
export const config: ApiRouteConfig = {
  type: 'api',
  name: 'TsHealthReviewAgent',
  path: '/ts/pets/:id/health-review',
  method: 'POST',
  emits: ['ts.health.treatment_required', 'ts.health.no_treatment_needed'],
  flows: ['TsPetManagement']
};
 
export const handler: Handlers['TsHealthReviewAgent'] = async (req, { emit, logger }) => {
  const petId = req.pathParams?.id;
 
  if (!petId) {
    return { status: 400, body: { message: 'Pet ID is required' } };
  }
 
  const pet = TSStore.get(petId);
  if (!pet) {
    return { status: 404, body: { message: 'Pet not found' } };
  }
 
  if (logger) {
    logger.info('🏥 Health Review Agent triggered', { 
      petId, 
      currentStatus: pet.status,
      symptoms: pet.symptoms || []
    });
  }
 
  if (!['healthy', 'in_quarantine', 'available'].includes(pet.status)) {
    return {
      status: 400,
      body: {
        message: 'Health review can only be performed on healthy, quarantined, or available pets',
        currentStatus: pet.status
      }
    };
  }
 
  const agentContext = buildAgentContext(pet);
 
  const recentArtifacts = getAgentArtifacts(petId)
    .filter(a => 
      a.agentType === 'health-review' && 
      a.success && 
      a.inputs.currentStatus === pet.status &&
      (Date.now() - a.timestamp) < 60000
    );
 
  if (recentArtifacts.length > 0) {
    const recent = recentArtifacts[recentArtifacts.length - 1];
    if (logger) {
      logger.info('🔄 Idempotent health review - returning cached decision', {
        petId,
        chosenEmit: recent.parsedDecision.chosenEmit,
        timestamp: recent.timestamp
      });
    }
 
    return {
      status: 200,
      body: {
        message: 'Health review completed (cached)',
        petId,
        agentDecision: recent.parsedDecision,
        artifact: {
          timestamp: recent.timestamp,
          success: recent.success
        }
      }
    };
  }
 
  try {
    if (logger) {
      logger.info('🔍 Starting agent decision call', { petId, agentContext });
    }
    
    const artifact = await callAgentDecision(
      'health-review',
      agentContext,
      HEALTH_REVIEW_EMITS,
      logger
    );
    
    if (logger) {
      logger.info('✅ Agent decision call completed', { petId, success: artifact.success });
    }
 
    if (!artifact.success) {
      if (logger) {
        logger.warn('⚠️ Agent decision failed, but returning error response', {
          petId,
          error: artifact.error
        });
      }
      
      return {
        status: 500,
        body: {
          message: 'Agent decision failed',
          error: artifact.error,
          petId,
          suggestion: 'Check OpenAI API key and try again'
        }
      };
    }
 
    const chosenEmitDef = HEALTH_REVIEW_EMITS.find(e => e.id === artifact.parsedDecision.chosenEmit);
    if (!chosenEmitDef) {
      return {
        status: 500,
        body: {
          message: 'Invalid emit chosen by agent',
          chosenEmit: artifact.parsedDecision.chosenEmit
        }
      };
    }
 
    if (emit) {
      (emit as any)({
        topic: chosenEmitDef.topic as 'ts.health.treatment_required' | 'ts.health.no_treatment_needed',
        data: {
          petId,
          event: chosenEmitDef.id.replace('emit.', ''),
          agentDecision: artifact.parsedDecision,
          timestamp: artifact.timestamp,
          context: agentContext
        }
      });
 
      if (logger) {
        logger.info('✅ Health review emit fired', {
          petId,
          chosenEmit: artifact.parsedDecision.chosenEmit,
          topic: chosenEmitDef.topic,
          rationale: artifact.parsedDecision.rationale
        });
      }
    }
 
    return {
      status: 200,
      body: {
        message: 'Health review completed',
        petId,
        agentDecision: artifact.parsedDecision,
        emitFired: chosenEmitDef.topic,
        artifact: {
          timestamp: artifact.timestamp,
          success: artifact.success,
          availableEmits: artifact.availableEmits.map(e => e.id)
        }
      }
    };
 
  } catch (error: any) {
    if (logger) {
      logger.error('❌ Health review agent error', {
        petId,
        error: error.message
      });
    }
 
    return {
      status: 500,
      body: {
        message: 'Health review failed',
        error: error.message,
        petId
      }
    };
  }
};

How Decision-Making Agentic Steps Work

This agentic step is fundamentally different from the enrichment agentic step:

  1. It's an API Step - Staff trigger it explicitly when they need a decision
  2. It defines an emits registry - Lists all possible actions the AI can choose from (in agent-decision-framework.ts/js)
  3. It calls the AI with context + options - The AI evaluates and picks one
  4. It fires the chosen emit - This emit goes to the orchestrator, changing workflow state
  5. It uses idempotency checking - Caches recent decisions to prevent duplicate AI calls

The framework functions (buildAgentContext, callAgentDecision, getAgentArtifacts) handle the OpenAI call and ensure the AI picks from valid options.


Testing the Health Review Agentic Step

The best way to test decision-making agentic steps is through Workbench. You can create pets, trigger the health review, and watch the AI make decisions in real-time.

Create a Pet

Use Workbench to create a pet. The AI enrichment will automatically trigger.

Prefer using curl?

curl -X POST http://localhost:3000/ts/pets \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Buddy",
    "species": "dog",
    "ageMonths": 36
  }'

Trigger the Health Review

In Workbench, test the health review endpoint to see the AI make a decision.

Using curl?

curl -X POST http://localhost:3000/ts/pets/1/health-review \
  -H "Content-Type: application/json"

ai-health-review-create-pet

You'll get a response like:

{
  "message": "Health review completed",
  "petId": "1",
  "agentDecision": {
    "chosenEmit": "emit.health.treatment_required",
    "rationale": "The pet shows concerning symptoms including coughing, lethargy, and loss of appetite. These symptoms suggest a potential respiratory infection or illness requiring veterinary attention."
  },
  "emitFired": "ts.health.treatment_required",
  "artifact": {
    "timestamp": 1234567890,
    "success": true,
    "availableEmits": ["emit.health.treatment_required", "emit.health.no_treatment_needed"]
  }
}

ai-health-review-reqd

The AI evaluates the pet's data and makes a decision. The emit it fires will trigger the orchestrator to handle the appropriate state transition.

Verify the Status Change

Check the pet status in Workbench to see the AI's decision reflected in the workflow state.

Using curl?

curl http://localhost:3000/ts/pets/1

ai-health-review-status

The pet's status has automatically changed based on the AI's decision!


Connecting Agentic Steps to the Orchestrator

The real power comes when your agentic steps integrate with a workflow orchestrator. The orchestrator subscribes to the events emitted by agentic steps and handles the actual state transitions.

The orchestrator configuration shows it subscribes to agentic step events:

steps/typescript/pet-lifecycle-orchestrator.step.ts
export const config = {
  type: 'event',
  name: 'TsPetLifecycleOrchestrator',
  description: 'Pet lifecycle state management with staff interaction points',
  subscribes: [
    'ts.feeding.reminder.completed',
    'ts.pet.status.update.requested',
    'ts.health.treatment_required',       // From Health Review Agentic Step
    'ts.health.no_treatment_needed',      // From Health Review Agentic Step
    'ts.adoption.needs_data',             // From Adoption Review Agentic Step
    'ts.adoption.ready'                   // From Adoption Review Agentic Step
  ],
  emits: [
    'ts.treatment.required',
    'ts.adoption.ready',
    'ts.treatment.completed'
  ],
  flows: ['TsPetManagement']
}
 
// The orchestrator has transition rules that handle agentic step events
const TRANSITION_RULES: TransitionRule[] = [
  // ... other rules ...
  
  // Agentic step-driven health transitions
  {
    from: ["healthy", "in_quarantine"],
    to: "ill",
    event: "health.treatment_required",
    description: "Agent assessment - pet requires medical treatment"
  },
  {
    from: ["healthy", "in_quarantine"],
    to: "healthy",
    event: "health.no_treatment_needed",
    description: "Agent assessment - pet remains healthy"
  },
  // Agentic step-driven adoption transitions
  {
    from: ["healthy"],
    to: "healthy",
    event: "adoption.needs_data",
    description: "Agent assessment - pet needs additional data before adoption",
    flagAction: { action: 'add', flag: 'needs_data' }
  },
  {
    from: ["healthy"],
    to: "available",
    event: "adoption.ready",
    description: "Agent assessment - pet ready for adoption",
    guards: ['no_needs_data_flag']
  }
]

🎉 Congratulations! You've built intelligent agentic workflows that make decisions and drive workflows. Your pet shelter now has automated intelligence that would have taken hundreds of lines of complex logic to implement manually.


What's Next?

Your pet shelter now has intelligent agentic workflows making decisions! But how do you give users real-time feedback while all this AI processing happens in the background?

In the final guide, we'll add Real-Time Streaming to provide live updates as your workflows execute:

  • Stream Configuration - Define stream schemas for type-safe updates
  • API with Streaming - Initialize streams and return immediately to clients
  • Background Job Streaming - Push real-time progress updates as jobs process
  • Agentic Step Streaming - Stream AI enrichment progress in real-time
  • Multi-Step Streaming - Multiple steps updating the same stream

Let's complete your system by adding real-time streaming capabilities!

Explore more examples in the Motia Examples Repository.

Need help? See our Community Resources for questions, examples, and discussions.