Motia Icon

Workflows

Learn how to build automated workflows that manage complex business logic with Motia

What You'll Build

A pet lifecycle management system that automatically guides pets through their journey at your shelter:

  • Automated Status Transitions - Pets move through stages automatically when conditions are met
  • Staff Decision Points - Critical checkpoints where staff make the calls
  • Smart Progressions - Some transitions trigger follow-up actions automatically
  • Validation Rules - Prevents invalid status changes to keep data consistent

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 workflow-orchestrator

Install dependencies:

npm install

Start the Workbench:

npm run dev

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


Project Structure

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 workflow orchestration 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 Workflows

So far, you've built API endpoints that respond to requests and background jobs that handle async tasks. But what about coordinating complex business processes that involve multiple steps and decision points?

That's where workflows come in. It's the conductor of your system - making sure things happen in the right order, at the right time, and only when it makes sense.

In our pet shelter example, a pet goes through many stages:

  • New arrivals need health checks
  • Healthy pets become available for adoption
  • Sick pets need treatment before they're ready
  • Adoption applications require staff approval

A workflow manages all these transitions, enforcing the rules and keeping everything consistent.


The Pet Lifecycle Journey

When you create a pet, it starts as new. Once the feeding reminder job completes, it automatically moves to in_quarantine. Staff then checks on it and marks it healthy, which automatically progresses to available. When someone wants to adopt, it goes pending, then finally adopted.

The key here is some transitions happen automatically (like healthyavailable), while others need staff approval (like in_quarantinehealthy).

What about sick pets?

If staff finds a pet is ill, it automatically moves to under_treatment. When staff marks it recovered, it chains through automatic transitions: recoveredhealthyavailable.

This mix of automatic progressions and human decision points is what makes workflows powerful - the system handles the routine stuff while keeping people in control of important calls.


Creating the Workflow

The workflow orchestrator is a single Event Step that manages all pet lifecycle transitions. Here's the complete implementation:

View on GitHub:

steps/typescript/pet-lifecycle-orchestrator.step.ts
// steps/typescript/pet-lifecycle-orchestrator.step.ts
import { EventConfig, Handlers } from 'motia';
import { TSStore, Pet } from './ts-store';
 
type LifecycleEvent = 
  | 'pet.created'
  | 'feeding.reminder.completed'
  | 'status.update.requested';
 
type TransitionRule = {
  from: Pet["status"][];
  to: Pet["status"];
  event: LifecycleEvent;
  description: string;
};
 
const TRANSITION_RULES: TransitionRule[] = [
  {
    from: ["new"],
    to: "in_quarantine",
    event: "feeding.reminder.completed",
    description: "Pet moved to quarantine after feeding setup"
  },
  {
    from: ["in_quarantine"],
    to: "healthy",
    event: "status.update.requested",
    description: "Staff health check - pet cleared from quarantine"
  },
  {
    from: ["healthy", "in_quarantine", "available"],
    to: "ill",
    event: "status.update.requested",
    description: "Staff assessment - pet identified as ill"
  },
  {
    from: ["healthy"],
    to: "available",
    event: "status.update.requested",
    description: "Staff decision - pet ready for adoption"
  },
  {
    from: ["ill"],
    to: "under_treatment",
    event: "status.update.requested",
    description: "Staff decision - treatment started"
  },
  {
    from: ["under_treatment"],
    to: "recovered",
    event: "status.update.requested",
    description: "Staff assessment - treatment completed"
  },
  {
    from: ["recovered"],
    to: "healthy",
    event: "status.update.requested",
    description: "Staff clearance - pet fully recovered"
  },
  {
    from: ["available"],
    to: "pending",
    event: "status.update.requested",
    description: "Adoption application received"
  },
  {
    from: ["pending"],
    to: "adopted",
    event: "status.update.requested",
    description: "Adoption completed"
  },
  {
    from: ["pending"],
    to: "available",
    event: "status.update.requested",
    description: "Adoption application rejected/cancelled"
  }
];
 
export const config = {
  type: 'event',
  name: 'TsPetLifecycleOrchestrator',
  description: 'Pet lifecycle state management with staff interaction points',
  subscribes: ['ts.pet.created', 'ts.feeding.reminder.completed', 'ts.pet.status.update.requested'],
  emits: [],
  flows: ['TsPetManagement']
};
 
export const handler: Handlers['TsPetLifecycleOrchestrator'] = async (input, { emit, logger }) => {
  const { petId, event: eventType, requestedStatus, automatic } = input;
 
  if (logger) {
    const logMessage = automatic ? '🤖 Automatic progression' : '🔄 Lifecycle orchestrator processing';
    logger.info(logMessage, { petId, eventType, requestedStatus, automatic });
  }
 
  try {
    const pet = TSStore.get(petId);
    if (!pet) {
      if (logger) {
        logger.error('❌ Pet not found for lifecycle transition', { petId, eventType });
      }
      return;
    }
 
    // For status update requests, find the rule based on requested status
    let rule;
    if (eventType === 'status.update.requested' && requestedStatus) {
      rule = TRANSITION_RULES.find(r => 
        r.event === eventType && 
        r.from.includes(pet.status) && 
        r.to === requestedStatus
      );
    } else {
      // For other events (like feeding.reminder.completed)
      rule = TRANSITION_RULES.find(r => 
        r.event === eventType && r.from.includes(pet.status)
      );
    }
 
    if (!rule) {
      const reason = eventType === 'status.update.requested' 
        ? `Invalid transition: cannot change from ${pet.status} to ${requestedStatus}`
        : `No transition rule found for ${eventType} from ${pet.status}`;
        
      if (logger) {
        logger.warn('⚠️ Transition rejected', { 
          petId, 
          currentStatus: pet.status, 
          requestedStatus,
          eventType,
          reason
        });
      }
      
      // Transition rejected - no event emission needed
      return;
    }
 
    // Check for idempotency
    if (pet.status === rule.to) {
      if (logger) {
        logger.info('✅ Already in target status', { 
          petId, 
          status: pet.status,
          eventType
        });
      }
      return;
    }
 
    // Apply the transition
    const oldStatus = pet.status;
    const updatedPet = TSStore.updateStatus(petId, rule.to);
    
    if (!updatedPet) {
      if (logger) {
        logger.error('❌ Failed to update pet status', { petId, oldStatus, newStatus: rule.to });
      }
      return;
    }
 
    if (logger) {
      logger.info('✅ Lifecycle transition completed', {
        petId,
        oldStatus,
        newStatus: rule.to,
        eventType,
        description: rule.description,
        timestamp: Date.now()
      });
    }
 
    // Transition completed successfully
    if (logger) {
      logger.info('✅ Pet status transition completed', { 
        petId, 
        oldStatus, 
        newStatus: rule.to, 
        eventType, 
        description: rule.description 
      });
    }
 
    // Check for automatic progressions after successful transition
    await processAutomaticProgression(petId, rule.to, emit, logger);
 
  } catch (error: any) {
    if (logger) {
      logger.error('❌ Lifecycle orchestrator error', { petId, eventType, error: error.message });
    }
  }
};
 
async function processAutomaticProgression(petId: string, currentStatus: Pet["status"], emit: any, logger: any) {
  // Define automatic progressions
  const automaticProgressions: Partial<Record<Pet["status"], { to: Pet["status"], description: string }>> = {
    'healthy': { to: 'available', description: 'Automatic progression - pet ready for adoption' },
    'ill': { to: 'under_treatment', description: 'Automatic progression - treatment started' },
    'recovered': { to: 'healthy', description: 'Automatic progression - recovery complete' }
  };
 
  const progression = automaticProgressions[currentStatus];
  if (progression) {
    if (logger) {
      logger.info('🤖 Processing automatic progression', { 
        petId, 
        currentStatus, 
        nextStatus: progression.to 
      });
    }
 
    // Find the transition rule for automatic progression
    const rule = TRANSITION_RULES.find(r => 
      r.event === 'status.update.requested' && 
      r.from.includes(currentStatus) && 
      r.to === progression.to
    );
 
    if (rule) {
      // Apply the automatic transition immediately
      const oldStatus = currentStatus;
      const updatedPet = TSStore.updateStatus(petId, rule.to);
      
      if (updatedPet) {
        if (logger) {
          logger.info('✅ Automatic progression completed', {
            petId,
            oldStatus,
            newStatus: rule.to,
            description: progression.description,
            timestamp: Date.now()
          });
        }
 
        // Automatic progression completed successfully
        if (logger) {
          logger.info('✅ Automatic progression completed', { 
            petId, 
            oldStatus, 
            newStatus: rule.to, 
            description: progression.description 
          });
        }
 
        // Check for further automatic progressions (for chaining like recovered → healthy → available)
        await processAutomaticProgression(petId, rule.to, emit, logger);
      } else if (logger) {
        logger.error('❌ Failed to apply automatic progression', { petId, oldStatus, newStatus: rule.to });
      }
    } else if (logger) {
      logger.warn('⚠️ No transition rule found for automatic progression', { 
        petId, 
        currentStatus, 
        targetStatus: progression.to 
      });
    }
  }
}

How the Orchestrator Works

The orchestrator has three main responsibilities:

  1. Validate Transitions - Ensures pets can only move to valid next statuses
  2. Apply Transitions - Updates the pet's status in the store
  3. Trigger Automatic Progressions - Some statuses automatically progress to the next stage

Key Points:

  • emits: [] - The orchestrator doesn't declare emits because it only manages state internally
  • JavaScript/Python emit events for workflow tracking (optional pattern)
  • TypeScript focuses purely on state management
  • All languages validate transitions using the same TRANSITION_RULES

Testing Your Orchestrator

The best way to test your orchestrator is through Workbench. It lets you send requests, watch the workflow execute in real-time, and see all the logs in one place.

Create a Pet

Open Workbench and test the CreatePet endpoint:

post-pet-test

You'll see in the logs:

🐾 Pet created { petId: '1', name: 'Max', species: 'dog', status: 'new' }
🔄 Setting next feeding reminder { petId: '1' }
✅ Next feeding reminder set { petId: '1' }
🔄 Lifecycle orchestrator processing { petId: '1', eventType: 'feeding.reminder.completed' }
✅ Lifecycle transition completed { oldStatus: 'new', newStatus: 'in_quarantine' }

Prefer using curl? You can also test with command line:

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

Staff Health Check

Test the UpdatePet endpoint in Workbench to mark the pet as healthy:

update-status-test

Watch the automatic progression:

👤 Staff requesting status change { petId: '1', requestedStatus: 'healthy' }
🔄 Lifecycle orchestrator processing { petId: '1', eventType: 'status.update.requested' }
✅ Lifecycle transition completed { oldStatus: 'in_quarantine', newStatus: 'healthy' }
🤖 Processing automatic progression { petId: '1', currentStatus: 'healthy', nextStatus: 'available' }
✅ Automatic progression completed { oldStatus: 'healthy', newStatus: 'available' }

Using curl?

curl -X PUT http://localhost:3000/ts/pets/1 \
  -H "Content-Type: application/json" \
  -d '{"status": "healthy"}'

Test Invalid Transitions

Try to skip a step in Workbench:

skip-status-test

The orchestrator rejects it:

⚠️ Transition rejected { 
  currentStatus: 'in_quarantine', 
  requestedStatus: 'available',
  reason: 'Invalid transition: cannot change from in_quarantine to available'
}

Using curl?

curl -X PUT http://localhost:3000/ts/pets/1 \
  -H "Content-Type: application/json" \
  -d '{"status": "available"}'

Test the Illness Workflow

Mark a pet as ill in Workbench:

update-status-ill-test

Watch the automatic treatment start:

✅ Lifecycle transition completed { oldStatus: 'healthy', newStatus: 'ill' }
🤖 Processing automatic progression { currentStatus: 'ill', nextStatus: 'under_treatment' }
✅ Automatic progression completed { oldStatus: 'ill', newStatus: 'under_treatment' }

Using curl?

curl -X PUT http://localhost:3000/ts/pets/1 \
  -H "Content-Type: application/json" \
  -d '{"status": "ill"}'

Then mark the pet as recovered in Workbench:

update-status-recovered-test

Watch the chained automatic progressions:

✅ Lifecycle transition completed { oldStatus: 'under_treatment', newStatus: 'recovered' }
🤖 Processing automatic progression { currentStatus: 'recovered', nextStatus: 'healthy' }
✅ Automatic progression completed { oldStatus: 'recovered', newStatus: 'healthy' }
🤖 Processing automatic progression { currentStatus: 'healthy', nextStatus: 'available' }
✅ Automatic progression completed { oldStatus: 'healthy', newStatus: 'available' }

Using curl?

curl -X PUT http://localhost:3000/ts/pets/1 \
  -H "Content-Type: application/json" \
  -d '{"status": "recovered"}'

Monitoring Your Orchestrator

Use the Workbench to visualize the entire flow:

Tracing

See how events flow through your system:

orchestrator-trace

Each trace shows:

  • The initial API call
  • Background job processing
  • Orchestrator transitions
  • Automatic progressions
  • Total time for each step

Logs

Filter by pet ID to see the complete lifecycle:

orchestrator-logs

The logs tell the story of each pet's journey through your shelter.

🎉 Congratulations! You've built a complete workflow orchestrator that manages complex business logic while keeping your code clean and maintainable.


What's Next?

Your pet shelter now has a complete backend system with workflow orchestration! But what about decisions that aren't black and white? Should this pet's symptoms require treatment?

In the next guide, we'll add Agentic Workflows that make intelligent decisions within your workflows:

  • Health Review Agentic Step - Analyzes symptoms and decides if treatment is needed
  • Adoption Review Agentic Step - Assesses if pets are ready for adoption
  • AI Profile Enrichment - Automatically generates engaging pet profiles
  • Agentic Decision Making - AI that chooses which workflow path to take

Let's continue building by adding intelligent decision-making to your workflows.

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