This commit is contained in:
2026-02-07 09:23:49 +00:00
parent 96eabe3db6
commit 36552903e7
85 changed files with 9820870 additions and 1767 deletions

View File

@@ -0,0 +1,32 @@
# Aider Configuration for Motia Projects
# https://aider.chat/docs/config.html
# Read AGENTS.md for project overview and guide references
read:
- AGENTS.md
# Uncomment specific guides as needed for more context:
# - .cursor/rules/motia/api-steps.mdc
# - .cursor/rules/motia/event-steps.mdc
# - .cursor/rules/motia/cron-steps.mdc
# - .cursor/rules/motia/state-management.mdc
# - .cursor/rules/motia/middlewares.mdc
# - .cursor/rules/motia/realtime-streaming.mdc
# - .cursor/rules/motia/virtual-steps.mdc
# - .cursor/rules/motia/ui-steps.mdc
# - .cursor/architecture/architecture.mdc
# - .cursor/architecture/error-handling.mdc
# Note: AGENTS.md references all detailed guides in .cursor/rules/
# The AI will read those guides when needed based on AGENTS.md instructions
# Auto-commit changes (optional, uncomment to enable)
# auto-commits: true
# Model selection (uncomment your preferred model)
# model: gpt-4
# model: claude-3-5-sonnet-20241022
# Additional context files (optional)
# read:
# - config.yml
# - package.json

View File

@@ -0,0 +1,104 @@
---
name: motia-developer
description: Expert Motia developer. Use PROACTIVELY for all Motia development tasks. References comprehensive cursor rules for patterns and best practices.
tools: Read, Edit, Write, Grep, Bash
model: inherit
---
You are an expert Motia developer with comprehensive knowledge of all Motia patterns.
## CRITICAL: Always Read Cursor Rules First
Before writing ANY Motia code, you MUST read the relevant cursor rules from `.cursor/rules/`:
### Configuration Guide (in `.cursor/rules/motia/`)
1. **`motia-config.mdc`** - Project configuration
- Package.json requirements (`"type": "module"`)
- Plugin naming conventions and setup
- Adapter configuration, Redis setup
- Stream authentication patterns
### Step Type Guides (in `.cursor/rules/motia/`)
2. **`api-steps.mdc`** - HTTP endpoints
- Creating API Steps with TypeScript, JavaScript, or Python
- Request/response schemas, validation, middleware
- When to emit events vs process directly
3. **`event-steps.mdc`** - Background tasks
- Creating Event Steps with TypeScript, JavaScript, or Python
- Topic subscription, event chaining, retry mechanisms
- Asynchronous workflow patterns
4. **`cron-steps.mdc`** - Scheduled tasks
- Creating Cron Steps with TypeScript, JavaScript, or Python
- Cron expression syntax, idempotent patterns
- When to emit events from scheduled jobs
5. **`state-management.mdc`** - State/cache management
- Using state across steps with TypeScript, JavaScript, or Python
- When to use state vs database
- TTL configuration, caching strategies
6. **`middlewares.mdc`** - Request/response middleware
- Creating middleware with TypeScript, JavaScript, or Python
- Authentication, validation, error handling
- Middleware composition patterns
7. **`realtime-streaming.mdc`** - Real-time data
- Server-Sent Events (SSE) patterns
- WebSocket support
- Stream configuration and usage
8. **`virtual-steps.mdc`** - Visual flow connections
- Creating NOOP steps for Workbench
- Virtual emits/subscribes for documentation
- Workflow visualization
9. **`ui-steps.mdc`** - Custom Workbench components
- Creating custom visual components (TypeScript/React)
- EventNode, ApiNode, CronNode components
- Styling with Tailwind
### Architecture Guides (in `.cursor/architecture/`)
10. **`architecture.mdc`** - Project structure
- File organization, naming conventions
- Domain-Driven Design patterns
- Services, repositories, utilities structure
11. **`error-handling.mdc`** - Error handling
- Custom error classes
- Middleware error handling
- ZodError/Pydantic validation errors
## Workflow
1. **Identify the task type** (API, Event, Cron, etc.)
2. **Read the relevant cursor rule(s)** from the list above
3. **Follow the patterns exactly** as shown in the guide
4. **Generate types** after config changes:
```bash
npx motia generate-types
```
## Key Principles
- **All guides have TypeScript, JavaScript, and Python examples**
- **Steps can live in `/src` or `/steps`** - Motia discovers both (use `/src` for modern structure)
- **Always export `config` and `handler`**
- **List all emits in config before using them**
- **Follow naming conventions**: `*.step.ts` (TS), `*.step.js` (JS), `*_step.py` (Python)
- **Use Domain-Driven Design**: Steps → Services → Repositories
## Never Guess
If you're unsure about any Motia pattern:
1. Read the relevant cursor rule from the list above
2. Check existing steps in the project
3. Follow the examples in the guides exactly
---
Remember: The 11 cursor rules in `.cursor/rules/` are your source of truth. Always read them first.

View File

@@ -0,0 +1,192 @@
---
description: How to structure your Motia project
globs:
alwaysApply: true
---
# Architecture Guide
## Overview
This guide covers the architecture and best practices for structuring Motia projects.
**Key Takeaway**: Motia automatically discovers steps from anywhere in your project. Modern projects use `/src` for a familiar structure that works seamlessly with Domain-Driven Design.
## File Structure
Motia automatically discovers step files from your project. You can organize steps in either:
- **`/src` folder** (recommended) - Familiar pattern for most developers
- **`/steps` folder** - Traditional Motia pattern
- Both folders simultaneously
### Recommended Structure (using `/src`)
```
project/
├── src/
│ ├── api/ # API endpoints
│ │ ├── users.step.ts
│ │ └── orders.step.ts
│ ├── events/ # Event handlers
│ │ ├── order-processing.step.ts
│ │ └── notifications.step.ts
│ ├── cron/ # Scheduled tasks
│ │ └── cleanup.step.ts
│ ├── services/ # Business logic
│ ├── repositories/ # Data access
│ └── utils/ # Utilities
└── motia.config.ts
```
### Alternative Structure (using `/steps`)
```
project/
├── steps/
│ ├── api/
│ │ └── users.step.ts
│ ├── events/
│ │ └── order-processing.step.ts
│ └── cron/
│ └── cleanup.step.ts
├── src/
│ ├── services/
│ └── utils/
└── motia.config.ts
```
Create subfolders within your chosen directory to organize related steps into logical groups (domains, features, or flows).
**Why `/src` is recommended:**
- Familiar to developers from other frameworks (Next.js, NestJS, etc.)
- Natural co-location with services, repositories, and utilities
- Works seamlessly with Domain-Driven Design patterns
- Cleaner project root with fewer top-level folders
## Step Naming Conventions
### Typescript
- Use kebab-case for filenames: `resource-processing.step.ts`
- Include `.step` before language extension
### Python
- Use snake_case for filenames: `data_processor_step.py`
- Include `_step` before language extension
### Global
- Match handler names to config names
- Use descriptive, action-oriented names
## Code Style Guidelines
- **JavaScript**: Use modern ES6+ features, async/await, proper error handling
- **TypeScript**: Make sure you use the correct Handlers type that is auto generated on the `types.d.ts` file.
## Defining Middlewares
Middleware is a powerful feature in Motia for common validation, error handling, and shared logic.
### Middleware Organization
Store middlewares in a dedicated folder:
- `/middlewares` at project root (recommended)
- `/src/middlewares` if using `/src` structure
### Best Practices
- **One responsibility per middleware** - Follow SOLID principles
- **Descriptive naming** - Use names like `auth.middleware.ts`, `validation.middleware.ts`
- **Handle errors gracefully** - Use core middleware for ZodError (see [Error Handling Guide](./error-handling.mdc))
- **Avoid infrastructure concerns** - Rate limiting and CORS are handled by infrastructure, not middleware
## Domain Driven Design
Motia encourages Domain-Driven Design (DDD) principles for maintainable, scalable applications.
### Folder Structure for DDD
When using `/src` for steps (recommended), your structure naturally supports DDD:
```
src/
├── api/ # API Steps (Controllers)
├── events/ # Event Steps (Controllers)
├── cron/ # Cron Steps (Controllers)
├── services/ # Business logic layer
├── repositories/ # Data access layer
├── utils/ # Utility functions
└── types/ # Shared types (optional)
```
### Layer Responsibilities
- **Steps (Controller Layer)**: Handle validation, call services, emit events
- **Services**: Contain business logic, orchestrate repositories
- **Repositories**: Direct data access (database, external APIs)
- **Utils**: Pure utility functions with no side effects
### Best Practices
- Models and DTOs are not necessary - use Zod schemas from step configs
- Steps should focus on validation and calling services
- Avoid service methods that only call repositories - Steps can access repositories directly
- Keep business logic in services, not in steps
### Services
Services contain your business logic and should be organized by domain.
**Structure:**
```
src/
├── services/
│ ├── auth/
│ │ ├── index.ts # Export service
│ │ ├── login.ts # Login method
│ │ └── register.ts # Register method
│ └── orders/
│ ├── index.ts
│ └── create-order.ts
└── api/
└── auth.step.ts # Uses authService
```
**Service Definition (`/src/services/auth/index.ts`):**
```typescript
/**
* Business logic methods imported from separate files
*/
import { login } from './login'
import { register } from './register'
/**
* Export service with methods as properties
*/
export const authService = {
login,
register
}
```
**Usage in Step (`/src/api/auth.step.ts`):**
```typescript
import { authService } from '../services/auth'
export const handler = async (req, ctx) => {
const user = await authService.login(req.body.email, req.body.password)
return { status: 200, body: { user } }
}
```
## Logging and observability
- Make sure to use the Logger from Motia (from context object) to log messages.
- Make sure to have visibility of what is going on in a request
- Before throwing errors, make sure to log the issue, identify if issue is a validation blocker, then log with `logger.warn`, if it's something that is not supposed to happen, then log with `logger.error`.
- Make sure to add context to the logs to help identify any potential issues.

View File

@@ -0,0 +1,122 @@
---
description: How to handle errors in a Motia project
globs:
alwaysApply: true
---
# Error Handling Guide
Errors happen, but we need to handle them gracefully. Make sure you create a custom error class for your project, underneath `/src/errors/` folder.
## Good practices
- Use Custom error to return errors to the client.
- Anything that is not the error class, should be logged with `logger.error`. And root cause should be omitted to the client.
## Create a custom Error class
Name: `/src/errors/base.error.ts`
```typescript
export class BaseError extends Error {
public readonly status: number
public readonly code: string
public readonly metadata: Record<string, any>
constructor(
message: string,
status: number = 500,
code: string = 'INTERNAL_SERVER_ERROR',
metadata: Record<string, any> = {}
) {
super(message)
this.name = this.constructor.name
this.status = status
this.code = code
this.metadata = metadata
// Maintains proper stack trace for where our error was thrown
Error.captureStackTrace(this, this.constructor)
}
toJSON() {
return {
error: {
name: this.name,
message: this.message,
code: this.code,
status: this.status,
...(Object.keys(this.metadata).length > 0 && { metadata: this.metadata }),
},
}
}
}
```
Then create sub class for specific errors that are commonly thrown in your project.
Name: `/src/errors/not-found.error.ts`
```typescript
import { BaseError } from './base.error'
export class NotFoundError extends BaseError {
constructor(message: string = 'Not Found', metadata: Record<string, any> = {}) {
super(message, 404, 'NOT_FOUND', metadata)
}
}
```
## Core Middleware
Make sure you create a core middleware that will be added to ALL API Steps.
File: `/src/middlewares/core.middleware.ts`
```typescript
import { ApiMiddleware } from 'motia'
import { ZodError } from 'zod'
import { BaseError } from '../errors/base.error'
export const coreMiddleware: ApiMiddleware = async (req, ctx, next) => {
const logger = ctx.logger
try {
return await next()
} catch (error: any) {
if (error instanceof ZodError) {
logger.error('Validation error', {
error,
stack: error.stack,
errors: error.errors,
})
return {
status: 400,
body: {
error: 'Invalid request body',
data: error.errors,
},
}
} else if (error instanceof BaseError) {
logger.error('BaseError', {
status: error.status,
code: error.code,
metadata: error.metadata,
name: error.name,
message: error.message,
})
return { status: error.status, body: error.toJSON() }
}
logger.error('Error while performing request', {
error,
body: req.body,
stack: error.stack,
})
return { status: 500, body: { error: 'Internal Server Error' } }
}
}
```

View File

@@ -0,0 +1,34 @@
---
description: Rules for the project
globs:
alwaysApply: true
---
## Real time events
Make sure to use [real time events guide](./rules/motia/realtime-streaming.mdc) to create a new real time event.
## State/Cache management
Make sure to use [state management guide](./rules/motia/state-management.mdc) to create a new state management.
## Creating HTTP Endpoints
Make sure to use [API steps guide](./rules/motia/api-steps.mdc) to create new HTTP endpoints.
## Background Tasks
Make sure to use [event steps guide](./rules/motia/event-steps.mdc) to create new background tasks.
## Scheduled Tasks
Make sure to use [cron steps guide](./rules/motia/cron-steps.mdc) to create new scheduled tasks.
## Virtual Steps & Flow Visualization
Make sure to use [virtual steps guide](./rules/motia/virtual-steps.mdc) when connecting nodes virtually and creating smooth flows in Workbench.
## Authentication
If ever need to add authentication, make sure to use middleware to authenticate the request.
Make sure to use [middlewares](./rules/motia/middlewares.mdc) to validate the requests.

View File

@@ -0,0 +1,441 @@
---
description: How to create HTTP endpoints in Motia
globs: steps/**/*.step.ts,steps/**/*.step.js,steps/**/*_step.py
alwaysApply: false
---
# API Steps Guide
API Steps expose HTTP endpoints that can trigger workflows and emit events.
## Creating API Steps
Steps need to be created in the `steps` folder, it can be in subfolders.
- Steps in TS and JS should end with `.step.ts` and `.step.js` respectively.
- Steps in Python should end with `_step.py`.
## Definition
Defining an API Step is done by two elements. Configuration and Handler.
### Schema Definition
- **TypeScript/JavaScript**: Motia uses Zod schemas for automatic validation of request/response data
- **Python**: Motia uses JSON Schema format. You can optionally use Pydantic models to generate JSON Schemas and handle manual validation in your handlers
### Configuration
**TypeScript/JavaScript**: You need to export a config constant via `export const config` that is a `ApiRouteConfig` type.
**Python**: You need to define a `config` dictionary with the same properties as the TypeScript `ApiRouteConfig`.
```typescript
export type ZodInput = ZodObject<any> | ZodArray<any>
export type StepSchemaInput = ZodInput | JsonSchema
export type Emit = string | {
/**
* The topic name to emit to.
*/
topic: string;
/**
* Optional label for the emission, could be used for documentation or UI.
*/
label?: string;
/**
* This is purely for documentation purposes,
* it doesn't affect the execution of the step.
*
* In Workbench, it will render differently based on this value.
*/
conditional?: boolean;
}
export interface QueryParam {
/**
* The name of the query parameter
*/
name: string
/**
* The description of the query parameter
*/
description: string
}
export interface ApiRouteConfig {
/**
* Should always be api
*/
type: 'api'
/**
* A unique name for this API step, used internally and for linking handlers.
*/
name: string
/**
* Optional human-readable description.
*/
description?: string
/**
* The URL path for this API endpoint (e.g., '/users/:id').
*/
path: string
/**
* The HTTP method for this route.
* POST, GET, PUT, DELETE, PATCH, OPTIONS, HEAD
*/
method: ApiRouteMethod
/**
* Topics this API step can emit events to.
* Important note: All emits in the handler need to be listed here.
*/
emits: Emit[]
/**
* Optional: Topics that are virtually emitted, perhaps for documentation or lineage,
* but not strictly required for execution.
*
* In Motia Workbench, they will show up as gray connections to other steps.
*/
virtualEmits?: Emit[]
/**
* Optional: Virtually subscribed topics.
*
* Used by API steps when we want to chain different HTTP requests
* that could happen sequentially
*/
virtualSubscribes?: string[]
/**
* Flows are used to group multiple steps to be visible in diagrams in Workbench
*/
flows?: string[]
/**
* List of middlewares that will be executed BEFORE the handler is called
*/
middleware?: ApiMiddleware<any, any, any>[]
/**
* Schema for the request body. Accepts either:
* - Zod schema (ZodObject or ZodArray)
* - JSON Schema object
*
* Note: This is not validated automatically, you need to validate it in the handler.
*/
bodySchema?: StepSchemaInput
/**
* Schema for response bodies. Accepts either:
* - Zod schema (ZodObject or ZodArray)
* - JSON Schema object
*
* The key (number) is the HTTP status code this endpoint can return and
* for each HTTP Status Code, you need to define a schema that defines the response body
*/
responseSchema?: Record<number, StepSchemaInput>
/**
* Mostly for documentation purposes, it will show up in Endpoints section in Workbench
*/
queryParams?: QueryParam[]
/**
* Files to include in the step bundle.
* Needs to be relative to the step file.
*/
includeFiles?: string[]
}
```
### Handler
The handler is a function that is exported via `export const handler` that is a `ApiRouteHandler` type.
#### Type Definition from Motia
```typescript
export interface ApiRequest<TBody = unknown> {
/**
* Key-value pairs of path parameters (e.g., from '/users/:id').
*/
pathParams: Record<string, string>
/**
* Key-value pairs of query string parameters. Values can be string or array of strings.
*/
queryParams: Record<string, string | string[]>
/**
* The parsed request body (typically an object if JSON, but can vary).
*/
body: TBody
/**
* Key-value pairs of request headers. Values can be string or array of strings.
*/
headers: Record<string, string | string[]>
}
export type ApiResponse<
TStatus extends number = number,
TBody = string | Buffer | Record<string, unknown>
> = {
status: TStatus
headers?: Record<string, string>
body: TBody
}
export type ApiRouteHandler<
/**
* The type defined by config['bodySchema']
*/
TRequestBody = unknown,
/**
* The type defined by config['responseSchema']
*/
TResponseBody extends ApiResponse<number, unknown> = ApiResponse<number, unknown>,
/**
* The type defined by config['emits'] which is dynamic depending
* on the topic handlers (Event Steps)
*/
TEmitData = never,
> = (req: ApiRequest<TRequestBody>, ctx: FlowContext<TEmitData>) => Promise<TResponseBody>
```
### Handler definition
**TypeScript/JavaScript:**
```typescript
export const handler: Handlers['CreateResource'] = async (req, { emit, logger, state, streams }) => {
// Implementation
}
```
**Python:**
```python
async def handler(req, context):
# req: dictionary containing pathParams, queryParams, body, headers
# context: object containing emit, logger, state, streams, trace_id
pass
```
### Examples
#### TypeScript Example
```typescript
import { ApiRouteConfig, Handlers } from 'motia';
import { z } from 'zod';
const bodySchema = z.object({
title: z.string().min(1, "Title cannot be empty"),
description: z.string().optional(),
category: z.string().min(1, "Category is required"),
metadata: z.record(z.string(), z.any()).optional()
})
export const config: ApiRouteConfig = {
name: 'CreateResource',
type: 'api',
path: '/resources',
method: 'POST',
emits: ['send-email'],
flows: ['resource-management'],
bodySchema,
responseSchema: {
201: z.object({
id: z.string(),
title: z.string(),
category: z.string()
}),
400: z.object({ error: z.string() })
}
};
export const handler: Handlers['CreateResource'] = async (req, { emit, logger }) => {
try {
const { title, description, category, metadata } = bodySchema.parse(req.body);
// Use the logger for structured logging. It's good practice to log key events or data.
logger.info('Attempting to create resource', { title, category });
/**
* Create files to manage service calls.
*
* Let's try to use Domain Driven Design to create files to manage service calls.
* Steps are the entry points, they're the Controllers on the DDD architecture.
*/
const result = await service.createResource(resourceData);
/**
* This is how we emit events to trigger Event Steps.
* Only use emits if the task can take a while to complete.
*
* Examples of long tasks are:
* - LLM Calls
* - Processing big files, like images, videos, audio, etc.
* - Sending emails
*
* Other applicable examples are tasks that are likely to fail, examples:
* - Webhook call to external systems
*
* API Calls that are okay to fail gracefully can be done without emits.
*/
await emit({
/**
* 'topic' must be one of the topics listed in config['emits'].
* do not emit to topics that are not defined in Steps
*/
topic: 'send-email',
/**
* 'data' is the payload of the event message.
* make sure the data used is compliant with the Event Step input schema
*/
data: {
/**
* The data to send to the Event Step.
*/
resource: result,
/**
* The user to send the email to.
*/
user
}
});
logger.info('Resource created successfully', { resourceId, title, category });
// Return a response object for the HTTP request.
return {
status: 201, // CREATED (specified in config['responseSchema'])
/**
* 'body' is the JSON response body sent back to the client.
*/
body: {
id: result.id,
title: result.title,
category: result.category,
status: 'active'
}
};
} catch (error) {
/**
* For one single step project, it is fine to
* handle ZodErrors here, on multiple steps projects,
* it is highly recommended to handle them as a middleware
* (defined in config['middleware'])
*/
if (error instanceof ZodError) {
logger.error('Resource creation failed', { error: error.message });
return {
status: 400,
body: { error: 'Validation failed' }
};
}
logger.error('Resource creation failed', { error: error.message });
return {
status: 500,
body: { error: 'Creation failed' }
};
}
};
```
#### Python Example
```python
from pydantic import BaseModel, Field
from typing import Optional, Dict, Any
class ResourceData(BaseModel):
title: str = Field(..., min_length=1, description="Title cannot be empty")
description: Optional[str] = None
category: str = Field(..., min_length=1, description="Category is required")
metadata: Optional[Dict[str, Any]] = None
class ResourceResponse(BaseModel):
id: str
title: str
category: str
status: str
class ErrorResponse(BaseModel):
error: str
config = {
"name": "CreateResource",
"type": "api",
"path": "/resources",
"method": "POST",
"emits": ["send-email"],
"flows": ["resource-management"],
"bodySchema": ResourceData.model_json_schema(),
"responseSchema": {
201: ResourceResponse.model_json_schema(),
400: ErrorResponse.model_json_schema()
}
}
async def handler(req, context):
try:
body = req.get("body", {})
# Optional: Validate input manually using Pydantic (Motia doesn't do this automatically)
resource_data = ResourceData(**body)
context.logger.info("Attempting to create resource", {
"title": resource_data.title,
"category": resource_data.category
})
# Process the resource creation
result = await service.create_resource({
"title": resource_data.title,
"description": resource_data.description,
"category": resource_data.category,
"metadata": resource_data.metadata
})
# Emit event to trigger other steps
await context.emit({
"topic": "send-email",
"data": {
"resource": result,
"user_id": "example-user"
}
})
context.logger.info("Resource created successfully", {
"resource_id": result.get("id"),
"title": result.get("title"),
"category": result.get("category")
})
return {
"status": 201,
"body": {
"id": result.get("id"),
"title": result.get("title"),
"category": result.get("category"),
"status": "active"
}
}
except ValidationError as e:
context.logger.error("Resource creation failed - Pydantic validation error", {"error": str(e)})
return {
"status": 400,
"body": {"error": "Validation failed"}
}
except Exception as e:
context.logger.error("Resource creation failed", {"error": str(e)})
return {
"status": 500,
"body": {"error": "Creation failed"}
}
```

View File

@@ -0,0 +1,171 @@
---
description: Cron Steps are scheduled tasks that run based on cron expressions.
globs: steps/**/*.step.ts,steps/**/*.step.js,steps/**/*_step.py
alwaysApply: false
---
# Cron Steps Guide
Cron Steps enable scheduled task execution using cron expressions.
They're typically used for recurring jobs like nightly reports, data synchronization, etc.
Cron steps can hold logic, but they do NOT have any retry mechanisms in place, if the logic
is likely to fail, it's recommended to use CRON Step to emit an event to a topic that will
ultimately trigger an Event Step that will handle the logic.
## Creating Cron Steps
Steps need to be created in the `steps` folder, it can be in subfolders.
- Steps in TS and JS should end with `.step.ts` and `.step.js` respectively
- Steps in Python should end with `_step.py`
## Definition
Defining a CRON Step is done by two elements. Configuration and Handler.
### Configuration
**TypeScript/JavaScript**: You need to export a config constant via `export const config` that is a `CronConfig` type.
**Python**: You need to define a `config` dictionary with the same properties as the TypeScript `CronConfig`.
```typescript
export type Emit = string | {
/**
* The topic name to emit to.
*/
topic: string;
/**
* Optional label for the emission, could be used for documentation or UI.
*/
label?: string;
/**
* This is purely for documentation purposes,
* it doesn't affect the execution of the step.
*
* In Workbench, it will render differently based on this value.
*/
conditional?: boolean;
}
export type CronConfig = {
/**
* Should always be cron
*/
type: 'cron'
/**
* A unique name for this cron step, used internally and for linking handlers.
*/
name: string
/**
* Optional human-readable description.
*/
description?: string
/**
* The cron expression for scheduling.
*/
cron: string
/**
* Optional: Topics that are virtually emitted, perhaps for documentation or lineage, but not strictly required for execution.
*/
virtualEmits?: Emit[]
/**
* Topics this cron step can emit events to.
*/
emits: Emit[]
/**
* Optional: An array of flow names this step belongs to.
*/
flows?: string[]
/**
* Files to include in the step bundle.
* Needs to be relative to the step file.
*/
includeFiles?: string[]
}
```
### Handler
The handler is a function that is exported via `export const handler` that is a `CronHandler` type.
**TypeScript/JavaScript:**
```typescript
/**
* CRON handler accepts only one argument, the FlowContext.
*
* The FlowContext is based on the Handlers which can vary depending on the config['emits'].
*/
export const handler: Handlers['CronJobEvery5Minutes'] = async ({ logger, emit, traceId, state, streams }) => {
logger.info('CRON Job Every 5 Minutes started')
}
```
**Python:**
```python
async def handler(context):
# context: object containing emit, logger, state, streams, trace_id
context.logger.info("CRON Job Every 5 Minutes started")
```
### Examples of Cron expressions
- `0 0 * * *`: Runs daily at midnight
- `*/5 * * * *`: Runs every 5 minutes
- `0 9 * * *`: Runs daily at 9 AM
- `0 9 * * 1`: Runs every Monday at 9 AM
- `0 9 * * 1-5`: Runs every Monday to Friday at 9 AM
### Example uses of Cron Steps
- Sending email notifications on a regular basis
- Cleaning up old records
- Purging old data
- Generating reports
- Sending out scheduled notifications
- Collecting metrics from third-party services
- Reconciling data from different sources
## Examples
### TypeScript Example
```typescript
export const config: CronConfig = {
name: 'CronJobEvery5Minutes', // should always be the same as Handlers['__']
type: 'cron',
cron: '*/5 * * * *',
emits: [], // No emits in this example
flows: ['example-flow']
};
export const handler: Handlers['CronJobEvery5Minutes'] = async ({ logger }) => {
logger.info('Cron job started')
}
```
### Python Example
```python
config = {
"name": "CronJobEvery5Minutes",
"type": "cron",
"cron": "*/5 * * * *", # Run every 5 minutes
"emits": [], # No emits in this example
"flows": ["example-flow"]
}
async def handler(context):
context.logger.info("Cron job started")
```

View File

@@ -0,0 +1,234 @@
---
description: How to create background tasks in Motia
globs: steps/**/*.step.ts,steps/**/*.step.js,steps/**/*_step.py
alwaysApply: false
---
# Event Steps Guide
Event Steps are used to handle asynchronous events. These steps cannot be
invoked by a client or user. In order to ultimately trigger an Event Step,
you need to connect it to an API Step or a CRON Step.
Examples of event steps are:
- LLM Calls
- Processing big files, like images, videos, audio, etc.
- Sending emails
Other applicable examples are tasks that are likely to fail, examples:
- Webhook call to external systems
## Creating Event Steps
Steps need to be created in the `steps` folder, it can be in subfolders.
- Steps in TS and JS should end with `.step.ts` and `.step.js` respectively.
- Steps in Python should end with `_step.py`.
## Definition
Defining an Event Step is done by two elements. Configuration and Handler.
### Schema Definition
- **TypeScript/JavaScript**: Motia uses Zod schemas for automatic validation of input data
- **Python**: Motia uses JSON Schema format. You can optionally use Pydantic models to generate JSON Schemas and handle manual validation in your handlers
### Configuration
**TypeScript/JavaScript**: You need to export a config constant via `export const config` that is a `EventConfig` type.
**Python**: You need to define a `config` dictionary with the same properties as the TypeScript `EventConfig`.
```typescript
export type ZodInput = ZodObject<any> | ZodArray<any>
export type StepSchemaInput = ZodInput | JsonSchema
export type Emit = string | {
/**
* The topic name to emit to.
*/
topic: string;
/**
* Optional label for the emission, could be used for documentation or UI.
*/
label?: string;
/**
* This is purely for documentation purposes,
* it doesn't affect the execution of the step.
*
* In Workbench, it will render differently based on this value.
*/
conditional?: boolean;
}
export type EventConfig = {
/**
* Should always be event
*/
type: 'event'
/**
* A unique name for this event step, used internally and for linking handlers.
*/
name: string
/**
* Optional human-readable description.
*/
description?: string
/**
* An array of topic names this step listens to.
*/
subscribes: string[]
/**
* An array of topics this step can emit events to.
*/
emits: Emit[]
/**
* Optional: Topics that are virtually emitted, perhaps for documentation or lineage, but not strictly required for execution.
*/
virtualEmits?: Emit[]
/**
* Optional: Virtually subscribed topics for documentation/lineage purposes.
*/
virtualSubscribes?: string[]
/**
* Schema for input data. Accepts either:
* - Zod schema (ZodObject or ZodArray)
* - JSON Schema object
*
* This is used by Motia to create the correct types for whoever emits the event
* to this step.
*
* Avoid adding too much data to the input schema, only add the data that
* is necessary for the Event Step to process. If the data is too big it's
* recommended to store it in the state and fetch it from the state on the
* Event Step handler.
*/
input?: StepSchemaInput
/**
* Optional: An array of flow names this step belongs to.
*/
flows?: string[]
/**
* Files to include in the step bundle.
* Needs to be relative to the step file.
*/
includeFiles?: string[]
/**
* Optional: Infrastructure configuration for handler and queue settings.
*/
infrastructure?: Partial<InfrastructureConfig>
}
```
### Handler
The handler is a function that is exported via `export const handler` that is a `EventHandler` type.
### Handler definition
**TypeScript/JavaScript:**
```typescript
/**
* Input is inferred from the Event Step config['input']
* Context is the FlowContext
*/
export const handler: Handlers['SendEmail'] = async (input, { emit, logger, state, streams }) => {
// Implementation
}
```
**Python:**
```python
async def handler(input_data, context):
# input_data: dictionary with the event data (matches the input schema)
# context: object containing emit, logger, state, streams, trace_id
pass
```
### Examples
#### TypeScript Example
```typescript
import { EventConfig, Handlers } from 'motia';
import { z } from 'zod';
const inputSchema = z.object({
email: z.string(),
templateId: z.string(),
templateData: z.record(z.string(), z.any()),
})
export const config: EventConfig = {
name: 'SendEmail',
type: 'event',
description: 'Sends email notification to the user',
subscribes: ['send-email'],
emits: [],
input: inputSchema,
flows: ['resource-management']
};
export const handler: Handlers['SendEmail'] = async (input, { emit, logger }) => {
const { email, templateId, templateData } = input;
// Process email sending logic here
await emailService.send({
to: email,
templateId,
data: templateData
});
logger.info('Email sent successfully', { email, templateId });
};
```
#### Python Example
```python
from pydantic import BaseModel
from typing import Dict, Any
class EmailData(BaseModel):
email: str
template_id: str
template_data: Dict[str, Any]
config = {
"name": "SendEmail",
"type": "event",
"description": "Sends email notification to the user",
"subscribes": ["send-email"],
"emits": [],
"input": EmailData.model_json_schema(),
"flows": ["resource-management"]
}
async def handler(input_data, context):
# Optional: Validate input manually using Pydantic (Motia doesn't do this automatically)
email_data = EmailData(**input_data)
# Process email sending logic here
await email_service.send({
"to": email_data.email,
"template_id": email_data.template_id,
"data": email_data.template_data
})
context.logger.info("Email sent successfully", {
"email": email_data.email,
"template_id": email_data.template_id
})
```

View File

@@ -0,0 +1,217 @@
---
description: Middlewares are used to execute code before and after the handler is called
globs: steps/**/*.step.ts,steps/**/*.step.js,steps/**/*_step.py,middlewares/**/*.middleware.ts,middlewares/**/*.middleware.js,middlewares/**/*_middleware.py
alwaysApply: false
---
# Middlewares Guide
Middlewares are used to execute code before and after the handler is called in API Steps.
The middleware is a handler that receives three arguments:
- **Request**: this is the same request object received by API Step handlers, if modified by the middleware, it will be the same object passed to the handler and any subsequent middleware. Be careful to not cause any side effects to the request object.
- **Context**: this is the same context object received by API Step handlers, if modified by the middleware, it will be the same object passed to the handler and any subsequent middleware. Be careful to not cause any side effects to the context object.
- **Next**: this is a function that you need to call to invoke the next middleware in the stack. If you don't call it, the request will be halted—the handler and any subsequent middlewares will not be called.
## Next function
Next function is a way to either continue the execution flow or stop it. For example, in authentication middlewares, if the user is not authenticated, you can return a 401 response and not call `next()`.
It can also be used to enrich data returned back to the HTTP response. Like adding a header parameter or so after calling `next()`.
## Adding middlewares to a step
### TypeScript/JavaScript Example
```typescript
import { ApiRouteConfig } from 'motia'
import { coreMiddleware } from '../middlewares/core.middleware'
export const config: ApiRouteConfig = {
type: 'api',
name: 'SampleRoute',
description: 'Sample route',
path: '/sample',
method: 'GET',
emits: [],
flows: [],
middleware: [coreMiddleware],
}
```
### Python Example
```python
async def enrich_data_middleware(req, context, next_fn):
context.logger.info("enriching data")
req["enriched"] = "yes"
return await next_fn()
config = {
"type": "api",
"name": "SampleRoute",
"description": "Sample route",
"path": "/sample",
"method": "GET",
"emits": [],
"flows": [],
"middleware": [enrich_data_middleware],
}
```
## Middleware examples
### Handling errors
#### TypeScript/JavaScript
```typescript
import { ApiMiddleware } from 'motia'
export const coreMiddleware: ApiMiddleware = async (req, ctx, next) => {
const { logger } = ctx
try {
/**
* Calling next() will invoke the next item in the stack.
*
* It will depend on the order of middlewares configured in the step,
* first one in the list is called first and so on.
*/
return await next()
} catch (error: any) {
logger.error('Error while performing request', {
error,
body: req.body, // make sure you don't include sensitive data in the logs
stack: error.stack,
})
return {
status: 500,
body: { error: 'Internal Server Error' },
}
}
}
```
#### Python
```python
async def error_handling_middleware(req, context, next_fn):
try:
# Calling next_fn() will invoke the next item in the stack.
# It will depend on the order of middlewares configured in the step,
# first one in the list is called first and so on.
return await next_fn()
except Exception as error:
context.logger.error('Error while performing request', {
'error': str(error),
'body': req.get('body'), # make sure you don't include sensitive data in the logs
})
return {
'status': 500,
'body': {'error': 'Internal Server Error'},
}
```
### Enriching response
#### TypeScript/JavaScript
```typescript
export const enrichResponseMiddleware: ApiMiddleware = async (req, ctx, next) => {
const response = await next()
response.headers['X-Custom-Header'] = 'Custom Value'
return response
}
```
#### Python
```python
async def enrich_response_middleware(req, context, next_fn):
response = await next_fn()
if not response.get('headers'):
response['headers'] = {}
response['headers']['X-Custom-Header'] = 'Custom Value'
return response
```
### Handling validation errors
#### TypeScript/JavaScript - Handling Zod Validation errors
```typescript
import { ApiMiddleware } from 'motia'
import { ZodError } from 'zod'
export const coreMiddleware: ApiMiddleware = async (req, ctx, next) => {
const logger = ctx.logger
try {
return await next()
} catch (error: any) {
if (error instanceof ZodError) {
logger.error('Validation error', {
error,
stack: error.stack,
errors: error.errors,
})
return {
status: 400,
body: {
error: 'Invalid request body',
data: error.errors,
},
}
}
logger.error('Error while performing request', {
error,
body: req.body, // make sure you don't include sensitive data in the logs
stack: error.stack,
})
return { status: 500, body: { error: 'Internal Server Error' } }
}
}
```
#### Python - Handling Pydantic Validation errors
```python
from pydantic import ValidationError
async def validation_middleware(req, context, next_fn):
try:
return await next_fn()
except ValidationError as error:
context.logger.error('Validation error', {
'error': str(error),
'errors': error.errors(),
})
return {
'status': 400,
'body': {
'error': 'Invalid request body',
'data': error.errors(),
},
}
except Exception as error:
context.logger.error('Error while performing request', {
'error': str(error),
'body': req.get('body'), # make sure you don't include sensitive data in the logs
})
return {
'status': 500,
'body': {'error': 'Internal Server Error'}
}
```

View File

@@ -0,0 +1,533 @@
---
description: Application configuration for Motia projects
globs: motia.config.ts,motia.config.js
alwaysApply: false
---
# Motia Configuration Guide
The `motia.config.ts` file is the central configuration file for your Motia application. It allows you to customize plugins, adapters, stream authentication, and Express app settings.
## Critical Requirement: package.json Must Use ES Modules
**All Motia projects MUST have `"type": "module"` in their `package.json`.**
Motia uses ES modules internally and requires this setting to function correctly. Without it, you may encounter import/export errors during runtime.
### Correct package.json Setup
```json
{
"name": "my-motia-project",
"description": "My Motia application",
"type": "module",
"scripts": {
"postinstall": "motia install",
"dev": "motia dev",
"start": "motia start",
"build": "motia build",
"generate-types": "motia generate-types"
}
}
```
### Migration from Existing Projects
If you have an existing Motia project, ensure you add `"type": "module"` to your `package.json`:
```json
{
"name": "my-project",
"type": "module", // ← Add this line
"scripts": {
"dev": "motia dev"
}
}
```
## Creating the Configuration File
Create a `motia.config.ts` file in the root of your project:
```typescript
import { config } from 'motia'
export default config({
plugins: [],
adapters: {},
streamAuth: undefined,
app: undefined,
})
```
## Creating Projects Without Embedded Redis
By default, Motia includes an embedded Redis binary for easy local development. However, if you prefer to use your own external Redis instance, you can use the `--skip-redis` flag when creating a new project:
```bash
npx motia create my-app --skip-redis
```
This flag:
- Skips the embedded Redis binary installation
- Creates a `motia.config.ts` pre-configured for external Redis connection
- Requires you to provide your own Redis instance before running Motia
When using `--skip-redis`, you'll need to ensure Redis is running and properly configured in your `motia.config.ts` file (see Redis Configuration section below).
## Type Definitions
```typescript
import type { Express } from 'express'
export type Config = {
/**
* Optional: Callback to customize the Express app instance.
* Use this to add custom middleware, routes, or configurations.
*/
app?: (app: Express) => void
/**
* Optional: Array of plugin builders to extend Motia functionality.
* Plugins can add workbench UI components and custom steps.
*/
plugins?: MotiaPluginBuilder[]
/**
* Optional: Custom adapters for state, events, cron, and streams.
* Use this for horizontal scaling or custom storage backends.
*/
adapters?: AdapterConfig
/**
* Optional: Stream authentication configuration.
* Use this to secure real-time stream subscriptions.
*/
streamAuth?: StreamAuthConfig
/**
* Optional: Redis configuration.
* Configure Redis connection or use the built-in in-memory Redis server.
*/
redis?: RedisConfig
}
export type AdapterConfig = {
state?: StateAdapter
streams?: StreamAdapterManager
events?: EventAdapter
cron?: CronAdapter
}
export type StreamAuthRequest = {
headers: Record<string, string | string[] | undefined>
url?: string
}
export type StreamAuthConfig<TSchema extends z.ZodTypeAny = z.ZodTypeAny> = {
/**
* JSON Schema defining the shape of the auth context.
* Use z.toJSONSchema() to convert a Zod schema.
*/
contextSchema: JsonSchema
/**
* Authentication callback that receives the request and returns
* the auth context or null if authentication fails.
*/
authenticate: (request: StreamAuthRequest) => Promise<z.infer<TSchema> | null> | (z.infer<TSchema> | null)
}
export type RedisConfig =
| {
useMemoryServer?: false
host: string
port: number
password?: string
username?: string
db?: number
}
| {
useMemoryServer: true
}
```
## Plugins
Plugins extend Motia functionality by adding workbench UI components and custom steps.
### Plugin Type Definition
```typescript
export type WorkbenchPlugin = {
packageName: string
componentName?: string
label?: string
labelIcon?: string
position?: 'bottom' | 'top'
cssImports?: string[]
props?: Record<string, any>
}
export type MotiaPlugin = {
workbench: WorkbenchPlugin[]
dirname?: string
steps?: string[]
}
export type MotiaPluginBuilder = (motia: MotiaPluginContext) => MotiaPlugin
```
### Using Built-in Plugins
```typescript
import { config } from 'motia'
import statesPlugin from '@motiadev/plugin-states/plugin'
import endpointPlugin from '@motiadev/plugin-endpoint/plugin'
import logsPlugin from '@motiadev/plugin-logs/plugin'
import observabilityPlugin from '@motiadev/plugin-observability/plugin'
export default config({
plugins: [observabilityPlugin, statesPlugin, endpointPlugin, logsPlugin],
})
```
### Creating a Local Plugin
**Project structure:**
```
project/
├── src/ # Steps can be in /src or /steps
│ └── api/
│ └── example.step.ts
├── plugins/
│ └── my-plugin/
│ ├── components/
│ │ └── my-plugin-panel.tsx
│ └── index.ts
└── motia.config.ts
```
**Plugin implementation (`plugins/my-plugin/index.ts`):**
```typescript
import path from 'node:path'
import { config, type MotiaPlugin, type MotiaPluginContext } from 'motia'
function myLocalPlugin(motia: MotiaPluginContext): MotiaPlugin {
motia.registerApi(
{
method: 'GET',
path: '/__motia/my-plugin',
},
async (_req, _ctx) => {
return {
status: 200,
body: { message: 'Hello from my plugin!' },
}
},
)
return {
dirname: path.join(__dirname, 'plugins'),
steps: ['**/*.step.ts'],
workbench: [
{
componentName: 'MyComponent',
packageName: '~/plugins/components/my-component',
label: 'My Plugin',
position: 'top',
labelIcon: 'toy-brick',
},
],
}
}
export default config({
plugins: [myLocalPlugin],
})
```
### Common Plugin Errors
**Error: Component not found**
- **Cause**: `packageName` doesn't match the actual folder structure
- **Solution**: Ensure `packageName: '~/plugins/my-plugin'` matches `plugins/my-plugin/` folder
**Error: Plugin not loading in workbench**
- **Cause**: Plugin function not exported correctly
- **Solution**: Use `export default function` in plugin's `index.ts`
**Error: Module resolution failed**
- **Cause**: Using incorrect casing in folder/file names
- **Solution**: Use `kebab-case` for folders/files, `PascalCase` for React components
## Adapters
Adapters allow you to customize the underlying infrastructure for state management, event handling, cron jobs, and streams. This is useful for horizontal scaling or using custom storage backends.
### Available Adapter Packages
| Package | Description |
|---------|-------------|
| `@motiadev/adapter-redis-state` | Redis-based state management for distributed systems |
| `@motiadev/adapter-redis-cron` | Redis-based cron scheduling with distributed locking |
| `@motiadev/adapter-redis-streams` | Redis Streams for real-time data |
| `@motiadev/adapter-rabbitmq-events` | RabbitMQ for event messaging |
| `@motiadev/adapter-bullmq-events` | BullMQ for event queue processing |
### Using Custom Adapters
```typescript
import { config } from 'motia'
import { RedisStateAdapter } from '@motiadev/adapter-redis-state'
import { RabbitMQEventAdapter } from '@motiadev/adapter-rabbitmq-events'
import { RedisCronAdapter } from '@motiadev/adapter-redis-cron'
export default config({
adapters: {
state: new RedisStateAdapter(
{ url: process.env.REDIS_URL },
{ keyPrefix: 'myapp:state:', ttl: 3600 }
),
events: new RabbitMQEventAdapter({
url: process.env.RABBITMQ_URL!,
exchangeType: 'topic',
exchangeName: 'motia-events',
}),
cron: new RedisCronAdapter(
{ url: process.env.REDIS_URL },
{ keyPrefix: 'myapp:cron:', lockTTL: 30000 }
),
},
})
```
## Stream Authentication
Stream authentication secures real-time stream subscriptions by validating client credentials.
### Configuration
```typescript
import { config, type StreamAuthRequest } from 'motia'
import { z } from 'zod'
const authContextSchema = z.object({
userId: z.string(),
permissions: z.array(z.string()).optional(),
})
export default config({
streamAuth: {
contextSchema: z.toJSONSchema(authContextSchema),
authenticate: async (request: StreamAuthRequest) => {
const token = extractToken(request)
if (!token) {
return null
}
const user = await validateToken(token)
if (!user) {
throw new Error('Invalid token')
}
return {
userId: user.id,
permissions: user.permissions,
}
},
},
})
function extractToken(request: StreamAuthRequest): string | undefined {
const protocol = request.headers['sec-websocket-protocol'] as string | undefined
if (protocol?.includes('Authorization')) {
const [, token] = protocol.split(',')
return token?.trim()
}
if (request.url) {
try {
const url = new URL(request.url)
return url.searchParams.get('authToken') ?? undefined
} catch {
return undefined
}
}
return undefined
}
```
### Using Auth Context in Streams
Once configured, the auth context is available in the `canAccess` callback of stream configurations:
```typescript
import { StreamConfig } from 'motia'
import { z } from 'zod'
export const config: StreamConfig = {
name: 'protectedStream',
schema: z.object({ data: z.string() }),
baseConfig: { storageType: 'default' },
canAccess: (subscription, authContext) => {
if (!authContext) return false
return authContext.permissions?.includes('read:stream')
},
}
```
## Express App Customization
Use the `app` callback to customize the Express application instance:
```typescript
import { config } from 'motia'
import cors from 'cors'
import helmet from 'helmet'
export default config({
app: (app) => {
app.use(helmet())
app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') }))
app.get('/health', (_req, res) => {
res.json({ status: 'healthy' })
})
},
})
```
## Redis Configuration
Motia uses Redis for state management, caching, and real-time features. By default, Motia automatically starts an in-memory Redis server for local development, eliminating the need for external Redis installation.
### Default Behavior (In-Memory Redis)
When no `redis` configuration is provided, Motia uses an embedded in-memory Redis server:
```typescript
import { config } from 'motia'
export default config({})
```
You can also explicitly enable the in-memory server:
```typescript
export default config({
redis: {
useMemoryServer: true,
},
})
```
### Using External Redis
To connect to an external Redis instance (useful for production or when you already have Redis running), configure the connection settings:
```typescript
import { config } from 'motia'
export default config({
redis: {
useMemoryServer: false,
host: 'localhost',
port: 6379,
},
})
```
For production environments with authentication:
```typescript
export default config({
redis: {
useMemoryServer: false,
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT || '6379'),
password: process.env.REDIS_PASSWORD,
username: process.env.REDIS_USERNAME,
db: parseInt(process.env.REDIS_DB || '0'),
},
})
```
The optional Redis configuration fields include:
- `password`: Redis password for authentication
- `username`: Redis username (Redis 6.0+)
- `db`: Database number to select (default: 0)
You can also use environment variables directly:
- `MOTIA_REDIS_PASSWORD`: Redis password
- `MOTIA_REDIS_USERNAME`: Redis username
- `MOTIA_REDIS_DB`: Database number
### Creating Projects with External Redis
When creating a new project, you can skip the embedded Redis binary installation using the `--skip-redis` flag:
```bash
npx motia create my-app --skip-redis
```
This will create a project with `motia.config.ts` pre-configured for external Redis, and you'll need to ensure Redis is running before starting your application.
## Complete Example
```typescript
import path from 'node:path'
import { config, type MotiaPlugin, type MotiaPluginContext, type StreamAuthRequest } from 'motia'
import { z } from 'zod'
const statesPlugin = require('@motiadev/plugin-states/plugin')
const logsPlugin = require('@motiadev/plugin-logs/plugin')
const authContextSchema = z.object({
userId: z.string(),
role: z.enum(['admin', 'user']).optional(),
})
type AuthContext = z.infer<typeof authContextSchema>
const tokens: Record<string, AuthContext> = {
'admin-token': { userId: 'admin-1', role: 'admin' },
'user-token': { userId: 'user-1', role: 'user' },
}
function extractAuthToken(request: StreamAuthRequest): string | undefined {
const protocol = request.headers['sec-websocket-protocol'] as string | undefined
if (protocol?.includes('Authorization')) {
const [, token] = protocol.split(',')
return token?.trim()
}
if (request.url) {
try {
const url = new URL(request.url)
return url.searchParams.get('authToken') ?? undefined
} catch {
return undefined
}
}
return undefined
}
export default config({
plugins: [statesPlugin, logsPlugin],
streamAuth: {
contextSchema: z.toJSONSchema(authContextSchema),
authenticate: async (request: StreamAuthRequest) => {
const token = extractAuthToken(request)
if (!token) return null
const context = tokens[token]
if (!context) throw new Error(`Invalid token: ${token}`)
return context
},
},
})
```

View File

@@ -0,0 +1,401 @@
---
description: Real-time streaming
globs: steps/**/*.step.ts,steps/**/*.step.js,steps/**/*_step.py,steps/**/*.stream.ts,steps/**/*.stream.js,steps/**/*_stream.py
alwaysApply: false
---
# Real-time Streaming
Building event driven applications often requires some real-time streaming capabilities.
- Like integration with LLMs, they should be implemented in an asynchronous way and updates should come in real-time.
- Chat applications, real-time collaboration, etc.
- Long living processes like data processing, video processing, etc.
Motia has a built-in real-time streaming system that allows you to easily implement real-time streaming capabilities in your application.
It's called Streams.
## Stream Configuration
Creating a Stream means defining a data schema that will be stored and served to the clients who are subscribing.
### StreamConfig Type Definition
```typescript
export type ZodInput = ZodObject<any> | ZodArray<any>
export type StepSchemaInput = ZodInput | JsonSchema
export type StreamSubscription = { groupId: string; id?: string }
export interface StreamConfig {
/**
* The stream name, used on the client side to subscribe.
*/
name: string
/**
* Schema for stream data. Accepts either:
* - Zod schema (ZodObject or ZodArray)
* - JSON Schema object
*/
schema: StepSchemaInput
/**
* Storage configuration for the stream.
* Use 'default' for built-in storage or 'custom' with a factory function.
*/
baseConfig: { storageType: 'default' } | { storageType: 'custom'; factory: () => MotiaStream<any> }
/**
* Optional: Access control callback to authorize subscriptions.
* Return true to allow access, false to deny.
*/
canAccess?: (subscription: StreamSubscription, authContext: any) => boolean | Promise<boolean>
}
```
### TypeScript Example
```typescript
// steps/streams/chat-messages.stream.ts
import { StreamConfig } from 'motia'
import { z } from 'zod'
export const chatMessageSchema = z.object({
id: z.string(),
userId: z.string(),
message: z.string(),
timestamp: z.string()
})
export type ChatMessage = z.infer<typeof chatMessageSchema>
export const config: StreamConfig = {
name: 'chatMessage',
schema: chatMessageSchema,
baseConfig: { storageType: 'default' },
}
```
### Python Examples
#### With Pydantic (Optional)
```python
# steps/streams/chat_messages_stream.py
from pydantic import BaseModel
class ChatMessage(BaseModel):
id: str
user_id: str
message: str
timestamp: str
config = {
"name": "chatMessage",
"schema": ChatMessage.model_json_schema(),
"baseConfig": {"storageType": "default"}
}
```
#### Without Pydantic (Pure JSON Schema)
```python
# steps/streams/chat_messages_stream.py
config = {
"name": "chatMessage",
"schema": {
"type": "object",
"properties": {
"id": {"type": "string"},
"user_id": {"type": "string"},
"message": {"type": "string"},
"timestamp": {"type": "string"}
},
"required": ["id", "user_id", "message", "timestamp"]
},
"baseConfig": {"storageType": "default"}
}
```
## Using streams
Streams managers are automatically injected into the context of the steps.
The interface of each stream is this
```typescript
export type BaseStreamItem<TData = unknown> = TData & { id: string }
interface MotiaStream<TData> {
/**
* Retrieves a single item from the stream
*
* @param groupId - The group id of the stream
* @param id - The id of the item to get
* @returns The item or null if it doesn't exist
*/
get(groupId: string, id: string): Promise<BaseStreamItem<TData> | null>
/**
* Create or update a single item in the stream.
*
* If the item doesn't exist, it will be created.
* If the item exists, it will be updated.
*
* @param groupId - The group id of the stream
* @param id - The id of the item to set
* @param data - The data to set
* @returns The item
*/
set(groupId: string, id: string, data: TData): Promise<BaseStreamItem<TData>>
/**
* Deletes a single item from the stream
*
* @param groupId - The group id of the stream
* @param id - The id of the item to delete
* @returns The item or null if it doesn't exist
*/
delete(groupId: string, id: string): Promise<BaseStreamItem<TData> | null>
/**
* Retrieves a group of items from the stream based on the group id
*
* @param groupId - The group id of the stream
* @returns The items
*/
getGroup(groupId: string): Promise<BaseStreamItem<TData>[]>
/**
* This is used mostly for ephemeral events in streams.
* A chat message for example has a state, which is the message content, user id, etc.
*
* However, if you want to send an event to the subscribers like:
* - online status
* - reactions
* - typing indicators
* - etc.
*
* @param channel - The channel to send the event to
* @param event - The event to send
*/
send<T>(channel: StateStreamEventChannel, event: StateStreamEvent<T>): Promise<void>
}
```
## Sending ephemeral events
Streams hold state, which means when the client connects and subscribes to a GroupID and ItemID,
they will automatically sync with the state of the stream, however, there might be cases where
you want to send an ephemeral event to the subscribers, like:
- online status
- reactions
- typing indicators
- etc.
This is where the `send` method comes in.
```typescript
/**
* The channel to send the event to
*/
type StateStreamEventChannel = {
/**
* The group id of the stream
*/
groupId: string
/**
* The id of the item to send the event to
*
* Optional, when not provided, the event will be sent to the entire group.
* Subscribers to the group will receive the event.
*/
id?: string
}
export type StateStreamEvent<TData> = {
/**
* The type of the event, use as the name of the event
* to be handled in the subscribers.
*/
type: string
/**
* The data of the event, the data that will be sent to the subscribers.
*/
data: TData
}
```
## Using in handlers
### TypeScript Example
```typescript
import { ApiRouteConfig, Handlers } from 'motia'
import { z } from 'zod'
import { chatMessageSchema } from './streams/chat-messages.stream'
export const config: ApiRouteConfig = {
name: 'CreateChatMessage',
type: 'api',
method: 'POST',
path: '/chat-messages',
bodySchema: z.object({
channelId: z.string(),
message: z.string(),
}),
emits: [],
responseSchema: {
201: chatMessageSchema
}
}
export const handler = async (req, { streams }) => {
/**
* Say this is an API Step that a user sends a message to a channel.
*
* In your application logic you should have a channel ID defined somewhere
* so the client can send the message to the correct channel.
*/
const { channelId, message } = req.body
/**
* Define the message ID however you want, but should be a unique identifier underneath the channel ID.
*
* This is used to identify the message in the stream.
*/
const messageId = crypto.randomUUID()
/**
* In your application logic you should have a user ID defined somewhere.
* We recommend using middlewares to identify the user on the request.
*/
const userId = 'example-user-id'
/**
* Creates a message in the stream
*/
const message = await streams.chatMessage.set(channelId, messageId, {
id: messageId,
userId: userId,
message: message,
timestamp: new Date().toISOString()
})
/**
* Returning the stream result directly to the client helps Workbench to
* render the stream object and update it in real-time in the UI.
*/
return { status: 201, body: message }
}
```
### Python Examples
#### With Pydantic (Optional)
```python
import uuid
from datetime import datetime
from pydantic import BaseModel
class ChatMessageRequest(BaseModel):
channel_id: str
message: str
class ChatMessageResponse(BaseModel):
id: str
user_id: str
message: str
timestamp: str
config = {
"name": "CreateChatMessage",
"type": "api",
"method": "POST",
"path": "/chat-messages",
"bodySchema": ChatMessageRequest.model_json_schema(),
"emits": [],
"responseSchema": {
201: ChatMessageResponse.model_json_schema()
}
}
async def handler(req, context):
body = req.get("body", {})
# Optional: Validate with Pydantic
request_data = ChatMessageRequest(**body)
channel_id = request_data.channel_id
message_text = request_data.message
message_id = str(uuid.uuid4())
user_id = "example-user-id"
# Creates a message in the stream
chat_message = await context.streams.chatMessage.set(channel_id, message_id, {
"id": message_id,
"user_id": user_id,
"message": message_text,
"timestamp": datetime.now().isoformat()
})
return {"status": 201, "body": chat_message}
```
#### Without Pydantic (Pure JSON Schema)
```python
import uuid
from datetime import datetime
config = {
"name": "CreateChatMessage",
"type": "api",
"method": "POST",
"path": "/chat-messages",
"bodySchema": {
"type": "object",
"properties": {
"channel_id": {"type": "string"},
"message": {"type": "string"}
},
"required": ["channel_id", "message"]
},
"emits": [],
"responseSchema": {
201: {
"type": "object",
"properties": {
"id": {"type": "string"},
"user_id": {"type": "string"},
"message": {"type": "string"},
"timestamp": {"type": "string"}
}
}
}
}
async def handler(req, context):
body = req.get("body", {})
channel_id = body.get("channel_id")
message_text = body.get("message")
message_id = str(uuid.uuid4())
user_id = "example-user-id"
# Creates a message in the stream
chat_message = await context.streams.chatMessage.set(channel_id, message_id, {
"id": message_id,
"user_id": user_id,
"message": message_text,
"timestamp": datetime.now().isoformat()
})
return {"status": 201, "body": chat_message}
```

View File

@@ -0,0 +1,136 @@
---
description: Managing state across Steps
globs: steps/**/*.step.ts,steps/**/*.step.js,steps/**/*_step.py
alwaysApply: false
---
# State Management
State Management is a core concept in Motia. It's used to store data across Steps.
They can be stored across different workflows.
If we want to trigger Event Steps, we can add data to the emit call, which can be used later in the Event Step execution. But this is limited and can't store too much data,
that's why we need to use the State Management to store data across Steps.
## Use-cases
**When State Management is recommended:**
- Pulling data from an external source, like an API, and storing it in the state, then triggering an Event Step to process the data.
- Storing data that needs to be used later in the workflow.
- Can be used for caching layer, like caching the result of an API call that usually can take a few seconds to complete and doesn't change very often.
**When another solution can be better suited:**
- Storing persistent user data: it's preferred to use a database like Postgres or MongoDB to store user data.
- Storing file data like Base64 encoded images, PDFs, etc: it's preferred to use a storage solution like S3, etc.
## StateManager Interface
The StateManager interface is available in the context of all step handlers. The methods work identically across TypeScript/JavaScript and Python.
```typescript
type InternalStateManager = {
/**
* Retrieves a single item from the state
*
* @param groupId - The group id of the state
* @param key - The key of the item to get
* @returns The item or null if it doesn't exist
*/
get<T>(groupId: string, key: string): Promise<T | null>
/**
* Sets a single item in the state
*
* @param groupId - The group id of the state
* @param key - The key of the item to set
* @param value - The value of the item to set
* @returns The item
*/
set<T>(groupId: string, key: string, value: T): Promise<T>
/**
* Deletes a single item from the state
*
* @param groupId - The group id of the state
* @param key - The key of the item to delete
* @returns The item or null if it doesn't exist
*/
delete<T>(groupId: string, key: string): Promise<T | null>
/**
* Retrieves a group of items from the state
*
* @param groupId - The group id of the state
* @returns A list with all the items in the group
*/
getGroup<T>(groupId: string): Promise<T[]>
/**
* Clears a group of items from the state
*
* @param groupId - The group id of the state
*/
clear(groupId: string): Promise<void>
}
```
## Usage Examples
### TypeScript/JavaScript Example
```typescript
export const handler: Handlers['ProcessOrder'] = async (input, { state, logger }) => {
// Store an order
const order = {
id: input.orderId,
status: 'processing',
createdAt: new Date().toISOString()
};
await state.set('orders', input.orderId, order);
// Retrieve an order
const savedOrder = await state.get('orders', input.orderId);
logger.info('Order retrieved', { savedOrder });
// Get all orders
const allOrders = await state.getGroup('orders');
logger.info('Total orders', { count: allOrders.length });
// Update order status
order.status = 'completed';
await state.set('orders', input.orderId, order);
// Delete an order (if needed)
// await state.delete('orders', input.orderId);
};
```
### Python Example
```python
async def handler(input_data, context):
# Store an order
order = {
"id": input_data.get("order_id"),
"status": "processing",
"created_at": datetime.now().isoformat()
}
await context.state.set("orders", input_data.get("order_id"), order)
# Retrieve an order
saved_order = await context.state.get("orders", input_data.get("order_id"))
context.logger.info("Order retrieved", {"saved_order": saved_order})
# Get all orders
all_orders = await context.state.get_group("orders")
context.logger.info("Total orders", {"count": len(all_orders)})
# Update order status
order["status"] = "completed"
await context.state.set("orders", input_data.get("order_id"), order)
# Delete an order (if needed)
# await context.state.delete("orders", input_data.get("order_id"))
```

View File

@@ -0,0 +1,76 @@
---
description: Overriding the UI of Steps in Motia
globs: steps/**/*.step.tsx,steps/**/*.step.jsx,steps/**/*_step.tsx,steps/**/*_step.jsx
alwaysApply: true
---
# UI Steps Guide for Motia
UI Steps provide a powerful way to create custom, visually appealing representations of your workflow steps in the Workbench flow visualization tool.
With UI Steps, you can enhance the user experience by designing intuitive, context-aware visual components that clearly communicate your flow's sequencing and events.
## Overview
To create a custom UI for a step, create a .tsx or .jsx file next to your step file with the same base name:
```
steps/
└── myStep/
├── myStep.step.ts # Step definition
└── myStep.step.tsx # Visual override
```
## Basic Usage
Let's override an EventNode but keeping the same look. Like the image below. We're going to add an image on the side and show the description.
```typescript
// myStep.step.tsx
import { EventNode, EventNodeProps } from 'motia/workbench'
import React from 'react'
export const Node: React.FC<EventNodeProps> = (props) => {
return (
<EventNode {...props}>
<div className="flex flex-row items-start gap-2">
<div className="text-sm text-gray-400 font-mono">{props.data.description}</div>
<img
style={{ width: '64px', height: '64px' }}
src="https://www.motia.dev/icon.png"
/>
</div>
</EventNode>
)
}
```
## Components
Motia Workbench provides out of the box components that you can use to create custom UI steps, which apply to different types of steps.
### Available Components
| Component | Props Type | Description |
|-----------|------------|-------------|
| EventNode | EventNodeProps | Base component for Event Steps, with built-in styling and connection points |
| ApiNode | ApiNodeProps | Component for API Steps, includes request/response visualization capabilities |
| CronNode | CronNodeProps | Base component for Cron Steps, displays timing information |
| NoopNode | NoopNodeProps | Base component for NoopNodes with a different color to comply workbench legend |
## Styling guidelines
- Use Tailwind's utility classes only: Stick to Tailwind CSS utilities for consistent styling
- Avoid arbitrary values: Use predefined scales from the design system
- Keep components responsive: Ensure UI elements adapt well to different screen sizes
- Follow Motia's design system: Maintain consistency with Motia's established design patterns
## Best practices
- Use base components: Use EventNode and ApiNode when possible
- Keep it simple: Maintain simple and clear visualizations
- Optimize performance: Minimize state and computations
- Documentation: Document custom components and patterns
- Style sharing: Share common styles through utility classes

View File

@@ -0,0 +1,251 @@
---
description: Connecting nodes virtually and creating a smooth flow in Workbench
globs: steps/**/*.step.ts,steps/**/*.step.js,steps/**/*_step.py
alwaysApply: false
---
# Virtual Steps Guide
Virtual Steps are useful for creating a smooth flow in Workbench.
They offer two ways to connect nodes virtually:
- NOOP Steps: Used mostly when we want to override the workflow to add buttons or show UI elements.
- Virtual Connections between steps: Used when we want to connect virtually two steps, with
or without NOOP Steps.
## Creating NOOP Steps
Steps need to be created in the `steps` folder, it can be in subfolders.
- Steps in TS and JS should end with `.step.ts` and `.step.js` respectively.
- Steps in Python should end with `_step.py`.
### Configuration
#### TypeScript Example
```typescript
import { NoopConfig } from 'motia'
export const config: NoopConfig = {
/**
* Should always be noop
*/
type: 'noop',
/**
* A unique name for this noop step, used internally and for linking handlers.
*/
name: 'manual-trigger',
/**
* A description for this noop step, used for documentation and UI.
*/
description: 'Manual trigger point for workflow',
/**
* An array of topics this step can emit events to.
*/
virtualEmits: ['workflow.start'],
/**
* An array of topics this step can subscribe to.
*/
virtualSubscribes: ['manual.trigger'],
/**
* An array of flow names this step belongs to.
*/
flows: ['my-workflow']
}
// NOOP steps don't need handlers - they're purely for Workbench workflow connections
```
#### Python Example
```python
config = {
"type": "noop",
"name": "manual-trigger",
"description": "Manual trigger point for workflow",
"virtualEmits": ["workflow.start"],
"virtualSubscribes": ["manual.trigger"],
"flows": ["my-workflow"]
}
# NOOP steps don't need handlers - they're purely for Workbench workflow connections
```
### Common Use Cases
#### Workflow Starter
This NOOP step will create a flow node in Workbench, then as a UI Step, we will override it
to show a button.
**TypeScript:**
```typescript
export const config: NoopConfig = {
type: 'noop',
name: 'flow-starter',
description: 'Start point for the workflow',
virtualEmits: ['process.begin'],
virtualSubscribes: [],
flows: ['main-flow']
}
```
**Python:**
```python
config = {
"type": "noop",
"name": "flow-starter",
"description": "Start point for the workflow",
"virtualEmits": ["process.begin"],
"virtualSubscribes": [],
"flows": ["main-flow"]
}
```
### Manual Approval Point
This NOOP step will create a flow node in Workbench, it's important just to connect
a previous Step to the step where it will have a manual approval button.
Example:
```mermaid
graph LR
A[Submit Article]
C[Approve Article]
D[Reject Article]
```
Without this NOOP Step, the steps A->C and A->D would all appear disconnected in Workbench.
**TypeScript:**
```typescript
export const config: NoopConfig = {
type: 'noop',
name: 'approval-gate',
description: 'Manual approval required',
virtualEmits: ['approved'],
virtualSubscribes: ['pending.approval'],
flows: ['approval-flow']
}
```
**Python:**
```python
config = {
"type": "noop",
"name": "approval-gate",
"description": "Manual approval required",
"virtualEmits": ["approved"],
"virtualSubscribes": ["pending.approval"],
"flows": ["approval-flow"]
}
```
## How it works
It uses the `virtualEmits` and `virtualSubscribes` to connect to the previous and next steps.
In `Submit Article` Step, it must have a `virtualEmits: ['approved']` to connect to the `Manual Approval` Step.
In `Approve Article` Step, it must have a `virtualSubscribes: ['pending.approval']` to connect to the `Submit Article` Step.
It's also possible to connect two Steps without NOOP just by connecting them directly.
It's also possible to use labels in connections.
**TypeScript Examples:**
```typescript
export const config: ApiRouteConfig = {
type: 'api',
name: 'CreateArticle',
path: '/articles',
method: 'POST',
description: 'Creates an article',
virtualEmits: [{ topic: 'approval.required', label: 'Requires approval' }],
emits: [],
flows: ['article']
}
export const config: ApiRouteConfig = {
type: 'api',
name: 'ApproveArticle',
path: '/articles/:id/approve',
method: 'POST',
description: 'Approves an article',
virtualSubscribes: ['approval.required'],
emits: [],
flows: ['article']
}
export const config: ApiRouteConfig = {
type: 'api',
name: 'RejectArticle',
path: '/articles/:id/reject',
method: 'POST',
description: 'Rejects an article',
virtualSubscribes: ['approval.required'],
emits: [],
flows: ['article']
}
```
**Python Examples:**
```python
# create_article_step.py
config = {
"type": "api",
"name": "CreateArticle",
"path": "/articles",
"method": "POST",
"description": "Creates an article",
"virtualEmits": [{"topic": "approval.required", "label": "Requires approval"}],
"emits": [],
"flows": ["article"]
}
# approve_article_step.py
config = {
"type": "api",
"name": "ApproveArticle",
"path": "/articles/:id/approve",
"method": "POST",
"description": "Approves an article",
"virtualSubscribes": ["approval.required"],
"emits": [],
"flows": ["article"]
}
# reject_article_step.py
config = {
"type": "api",
"name": "RejectArticle",
"path": "/articles/:id/reject",
"method": "POST",
"description": "Rejects an article",
"virtualSubscribes": ["approval.required"],
"emits": [],
"flows": ["article"]
}
```
This will create connection to two API Steps.
```mermaid
graph LR
A[Create Article] --> B[Requires Approval]
B --> C[Approve Article]
B --> D[Reject Article]
```
## When to Use NOOP Steps
- Testing workflow connections
- Manual trigger points
- Workflow visualization
- Placeholder for future steps

View File

@@ -0,0 +1,8 @@
node_modules
python_modules
.venv
venv
.motia
.mermaid
dist
*.pyc

View File

@@ -0,0 +1,230 @@
# AGENTS.md
> AI Development Guide for Motia Projects
This file provides context and instructions for AI coding assistants working on Motia projects.
## Project Overview
This is a **Motia** application - a framework for building event-driven, type-safe backend systems with:
- HTTP API endpoints (API Steps)
- Background event processing (Event Steps)
- Scheduled tasks (Cron Steps)
- Real-time streaming capabilities
- Built-in state management
- Visual workflow designer (Workbench)
## Quick Start Commands
```bash
# Install dependencies
npm install
# Start development server (with hot reload)
npm run dev
# Start production server (without hot reload)
npm run start
# Generate TypeScript types from steps
npx motia generate-types
```
## 📚 Comprehensive Guides
**This project includes detailed Cursor rules in `.cursor/rules/` that contain comprehensive patterns and examples.**
These guides are written in markdown and can be read by any AI coding tool. The sections below provide quick reference, but **always consult the detailed guides in `.cursor/` for complete patterns and examples.**
### Available Guides
Read these files in `.cursor/rules/motia/` for detailed patterns:
- **`motia-config.mdc`** - Essential project setup, package.json requirements, plugin naming
- **`api-steps.mdc`** - Creating HTTP endpoints with schemas, validation, and middleware
- **`event-steps.mdc`** - Background task processing and event-driven workflows
- **`cron-steps.mdc`** - Scheduled tasks with cron expressions
- **`state-management.mdc`** - State/cache management across steps
- **`middlewares.mdc`** - Request/response middleware patterns
- **`realtime-streaming.mdc`** - WebSocket and SSE patterns
- **`virtual-steps.mdc`** - Visual flow connections in Workbench
- **`ui-steps.mdc`** - Custom visual components for Workbench
Architecture guides in `.cursor/architecture/`:
- **`architecture.mdc`** - Project structure, naming conventions, DDD patterns
- **`error-handling.mdc`** - Error handling best practices
**Read these guides before writing code.** They contain complete examples, type definitions, and best practices.
## Quick Reference
> **⚠️ Important**: The sections below are brief summaries. **Always read the full guides in `.cursor/rules/` for complete patterns, examples, and type definitions.**
### Project Structure
Motia discovers steps from both `/src` and `/steps` folders. Modern projects typically use `/src`:
**Recommended Structure (using `/src`):**
```
project/
├── .cursor/rules/ # DETAILED GUIDES - Read these first!
├── src/
│ ├── api/ # API endpoints
│ │ ├── users.step.ts
│ │ ├── orders.step.js
│ │ └── products_step.py
│ ├── events/ # Event handlers
│ │ ├── order-processing.step.ts
│ │ └── notifications_step.py
│ ├── cron/ # Scheduled tasks
│ │ └── cleanup.step.ts
│ ├── services/ # Business logic
│ ├── repositories/ # Data access
│ └── utils/ # Utilities
├── middlewares/ # Reusable middleware
│ └── auth.middleware.ts
├── motia.config.ts # Motia configuration
└── types.d.ts # Auto-generated types
```
**Alternative Structure (using `/steps`):**
```
project/
├── steps/ # Step definitions
│ ├── api/
│ ├── events/
│ └── cron/
├── src/
│ ├── services/
│ └── utils/
└── motia.config.ts
```
### Step Naming Conventions
**TypeScript/JavaScript:** `my-step.step.ts` (kebab-case)
**Python:** `my_step_step.py` (snake_case)
See `.cursor/architecture/architecture.mdc` for complete naming rules.
### Creating Steps - Quick Start
Every step needs two exports:
1. **`config`** - Defines type, routing, schemas, emits
2. **`handler`** - Async function with processing logic
**For complete examples and type definitions, read:**
- `.cursor/rules/motia/api-steps.mdc` - HTTP endpoints
- `.cursor/rules/motia/event-steps.mdc` - Background tasks
- `.cursor/rules/motia/cron-steps.mdc` - Scheduled tasks
## Detailed Guides by Topic
> **📖 Read the cursor rules for complete information**
### Step Types
- **API Steps** → Read `.cursor/rules/motia/api-steps.mdc`
- HTTP endpoints, schemas, middleware, emits
- Complete TypeScript and Python examples
- When to use emits vs direct processing
- **Event Steps** → Read `.cursor/rules/motia/event-steps.mdc`
- Background processing, topic subscriptions
- Retry mechanisms, error handling
- Chaining events for complex workflows
- **Cron Steps** → Read `.cursor/rules/motia/cron-steps.mdc`
- Scheduled tasks with cron expressions
- Idempotent execution patterns
- Integration with event emits
### Architecture
- **Project Structure** → Read `.cursor/architecture/architecture.mdc`
- File organization, naming conventions
- Domain-Driven Design patterns (services, repositories)
- Code style guidelines for TypeScript, JavaScript, Python
- **Error Handling** → Read `.cursor/architecture/error-handling.mdc`
- ZodError middleware patterns
- Logging best practices
- HTTP status codes
### Advanced Features
- **State Management** → Read `.cursor/rules/motia/state-management.mdc`
- Caching strategies, TTL configuration
- When to use state vs database
- Complete API reference
- **Middlewares** → Read `.cursor/rules/motia/middlewares.mdc`
- Authentication, validation, error handling
- Creating reusable middleware
- Middleware composition
- **Real-time Streaming** → Read `.cursor/rules/motia/realtime-streaming.mdc`
- Server-Sent Events (SSE) patterns
- WebSocket support
- Client-side integration
- **Virtual Steps** → Read `.cursor/rules/motia/virtual-steps.mdc`
- Visual flow connections in Workbench
- Documenting API chains
- Flow organization
- **UI Steps** → Read `.cursor/rules/motia/ui-steps.mdc`
- Custom Workbench visualizations
- Available components (EventNode, ApiNode, etc.)
- Styling with Tailwind
## Workflow for AI Coding Assistants
When working on Motia projects, follow this pattern:
1. **Read the relevant guide** in `.cursor/rules/` for the task
- Creating API? Read `api-steps.mdc`
- Background task? Read `event-steps.mdc`
- Scheduled job? Read `cron-steps.mdc`
2. **Check the architecture guide** in `.cursor/architecture/architecture.mdc`
- Understand project structure
- Follow naming conventions
- Apply DDD patterns
3. **Implement following the patterns** from the guides
- Use the examples as templates
- Follow type definitions exactly
- Apply best practices
4. **Generate types** after changes
```bash
npx motia generate-types
```
5. **Test in Workbench** to verify connections
```bash
npx motia dev
```
## Critical Rules
- **ALWAYS** ensure `package.json` has `"type": "module"` (read `motia-config.mdc` for details)
- **ALWAYS** read `.cursor/rules/` guides before writing step code
- **ALWAYS** run `npx motia generate-types` after modifying configs
- **ALWAYS** list emits in config before using them in handlers
- **ALWAYS** follow naming conventions (`*.step.ts` or `*_step.py`)
- **NEVER** use API steps for background work (use Event steps)
- **NEVER** skip middleware for ZodError handling in multi-step projects
- **NEVER** implement rate limiting/CORS in code (infrastructure handles this)
## Resources
- **Detailed Guides**: `.cursor/rules/motia/*.mdc` (in this project)
- **Architecture**: `.cursor/architecture/*.mdc` (in this project)
- **Documentation**: [motia.dev/docs](https://motia.dev/docs)
- **Examples**: [motia.dev/docs/examples](https://motia.dev/docs/examples)
- **GitHub**: [github.com/MotiaDev/motia](https://github.com/MotiaDev/motia)
---
**Remember**: This AGENTS.md is a quick reference. The `.cursor/rules/` directory contains the comprehensive, authoritative guides with complete examples and type definitions. Always consult those guides when implementing Motia patterns.

View File

@@ -0,0 +1,63 @@
# Motia Project Guide for Claude Code & Claude AI
This project uses **Motia** - a framework for building event-driven, type-safe backend systems.
## 📚 Important: Read the Comprehensive Guides
This project has detailed development guides in **`.cursor/rules/`** directory. These markdown files (`.mdc`) contain complete patterns, examples, and type definitions.
**Before writing any Motia code, read the relevant guides from `.cursor/rules/`**
### For Claude Code Users
**A pre-configured subagent is ready!**
The `motia-developer` subagent in `.claude/agents/` automatically references all 11 cursor rules when coding.
Use it: `/agents` → select `motia-developer`
Learn more: [Claude Code Subagents Docs](https://docs.claude.com/en/docs/claude-code/sub-agents)
### For Claude AI Assistant (Chat)
Explicitly reference cursor rules in your prompts:
```
Read .cursor/rules/motia/api-steps.mdc and create an API endpoint
for user registration following the patterns shown.
```
## Available Guides (11 Comprehensive Files)
All guides in `.cursor/rules/` with **TypeScript, JavaScript, and Python** examples:
**Configuration** (`.cursor/rules/motia/`):
- `motia-config.mdc` - Essential project setup, package.json requirements, plugin naming
**Step Types** (`.cursor/rules/motia/`):
- `api-steps.mdc`, `event-steps.mdc`, `cron-steps.mdc`
**Features** (`.cursor/rules/motia/`):
- `state-management.mdc`, `middlewares.mdc`, `realtime-streaming.mdc`
- `virtual-steps.mdc`, `ui-steps.mdc`
**Architecture** (`.cursor/architecture/`):
- `architecture.mdc`, `error-handling.mdc`
## Quick Reference
See `AGENTS.md` in this directory for a quick overview and links to specific guides.
**Important**: Motia discovers steps from both `/src` and `/steps` folders. Modern projects use `/src` for a familiar structure.
## Key Commands
```bash
npm run dev # Start development server (with hot reload)
npm run start # Start production server (without hot reload)
npx motia generate-types # Regenerate TypeScript types
```
---
**Remember**: The `.cursor/rules/` directory is your primary reference. Read the relevant guide before implementing any Motia pattern.

View File

@@ -0,0 +1,84 @@
# motia-clean-test
A Motia project created with the starter template.
## What is Motia?
Motia is an open-source, unified backend framework that eliminates runtime fragmentation by bringing **APIs, background jobs, queueing, streaming, state, workflows, AI agents, observability, scaling, and deployment** into one unified system using a single core primitive, the **Step**.
## Quick Start
```bash
# Start the development server
npm run dev
# or
yarn dev
# or
pnpm dev
```
This starts the Motia runtime and the **Workbench** - a powerful UI for developing and debugging your workflows. By default, it's available at [`http://localhost:3000`](http://localhost:3000).
```bash
# Test your first endpoint
curl http://localhost:3000/hello
```
## Step Types
Every Step has a `type` that defines how it triggers:
| Type | When it runs | Use case |
|------|--------------|----------|
| **`api`** | HTTP request | REST APIs, webhooks |
| **`event`** | Event emitted | Background jobs, workflows |
| **`cron`** | Schedule | Cleanup, reports, reminders |
## Development Commands
```bash
# Start Workbench and development server
npm run dev
# or
yarn dev
# or
pnpm dev
# Start production server (without hot reload)
npm run start
# or
yarn start
# or
pnpm start
# Generate TypeScript types from Step configs
npm run generate-types
# or
yarn generate-types
# or
pnpm generate-types
# Build project for deployment
npm run build
# or
yarn build
# or
pnpm build
```
## Project Structure
```
steps/ # Your Step definitions (or use src/)
motia.config.ts # Motia configuration
requirements.txt # Python dependencies
```
Steps are auto-discovered from your `steps/` or `src/` directories - no manual registration required. You can write Steps in Python, TypeScript, or JavaScript, all in the same project.
## Learn More
- [Documentation](https://motia.dev/docs) - Complete guides and API reference
- [Quick Start Guide](https://motia.dev/docs/getting-started/quick-start) - Detailed getting started tutorial
- [Core Concepts](https://motia.dev/docs/concepts/overview) - Learn about Steps and Motia architecture
- [Discord Community](https://discord.gg/motia) - Get help and connect with other developers

View File

@@ -0,0 +1,29 @@
[
{
"id": "hello-world-flow",
"config": {
"src/hello/process_greeting_step.py": {
"x": 409,
"y": 44
},
"src/hello/hello_api_step.py": {
"x": 0,
"y": 0,
"sourceHandlePosition": "right"
}
}
},
{
"id": "perf-test",
"config": {
"src/perf-test/perf_event_step.py": {
"x": 319,
"y": 22
},
"src/perf-test/perf_cron_step.py": {
"x": 0,
"y": 0
}
}
}
]

View File

@@ -0,0 +1,10 @@
import { defineConfig } from '@motiadev/core'
import endpointPlugin from '@motiadev/plugin-endpoint/plugin'
import logsPlugin from '@motiadev/plugin-logs/plugin'
import observabilityPlugin from '@motiadev/plugin-observability/plugin'
import statesPlugin from '@motiadev/plugin-states/plugin'
import bullmqPlugin from '@motiadev/plugin-bullmq/plugin'
export default defineConfig({
plugins: [observabilityPlugin, statesPlugin, endpointPlugin, logsPlugin, bullmqPlugin],
})

View File

@@ -0,0 +1,19 @@
{
"name": "Motia Project",
"description": "Motia event-driven backend framework project",
"rules": ["AGENTS.md"],
"context": [
".cursor/rules/motia/motia-config.mdc",
".cursor/rules/motia/api-steps.mdc",
".cursor/rules/motia/event-steps.mdc",
".cursor/rules/motia/cron-steps.mdc",
".cursor/rules/motia/realtime-streaming.mdc",
".cursor/rules/motia/virtual-steps.mdc",
".cursor/rules/motia/ui-steps.mdc",
".cursor/rules/motia/state-management.mdc",
".cursor/rules/motia/middlewares.mdc",
".cursor/architecture/architecture.mdc",
".cursor/architecture/error-handling.mdc"
],
"notes": "Read AGENTS.md first - it references all detailed guides in .cursor/rules/ directory"
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
{
"name": "motia-clean-test",
"description": "",
"type": "module",
"scripts": {
"postinstall": "motia install",
"dev": "motia dev",
"start": "motia start",
"generate-types": "motia generate-types",
"build": "motia build",
"clean": "rm -rf dist node_modules python_modules .motia .mermaid"
},
"keywords": [
"motia"
],
"dependencies": {
"@motiadev/adapter-bullmq-events": "^0.17.11-beta.193",
"@motiadev/core": "^0.17.11-beta.193",
"@motiadev/plugin-bullmq": "^0.17.11-beta.193",
"@motiadev/plugin-endpoint": "^0.17.11-beta.193",
"@motiadev/plugin-logs": "^0.17.11-beta.193",
"@motiadev/plugin-observability": "^0.17.11-beta.193",
"@motiadev/plugin-states": "^0.17.11-beta.193",
"motia": "^0.17.11-beta.193",
"zod": "^4.1.12"
},
"devDependencies": {
"@motiadev/workbench": "^0.17.11-beta.193",
"@types/react": "^19.1.1",
"ts-node": "^10.9.2",
"typescript": "^5.7.3"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

View File

@@ -0,0 +1,952 @@
timestamp,type,batch_start,emission_duration,event_id,event_duration
14:42:00,cron_start,2025-12-31,,,
14:43:00,cron_start,2025-12-31,,,
14:44:00,cron_start,2025-12-31,,,
14:45:00,cron_start,2025-12-31,,,
14:46:00,cron_start,2025-12-31,,,
14:47:04,cron_start,2025-12-31,,,
14:48:34,cron_start,2025-12-31,,,
14:50:35,cron_start,2025-12-31,,,
14:53:12,cron_start,2025-12-31,,,
14:56:36,cron_start,2025-12-31,,,
14:42:01,emission,,1.135,,
14:43:03,emission,,3.452,,
14:44:04,emission,,4.422,,
14:45:06,emission,,6.048,,
14:46:08,emission,,7.921,,
14:47:14,emission,,9.359,,
14:48:45,emission,,11.004,,
14:50:48,emission,,12.66,,
14:53:26,emission,,14.074,,
14:42:00,event,,,0,3.16
14:42:00,event,,,1,4.01
14:42:00,event,,,2,3.25
14:42:00,event,,,3,1.57
14:42:00,event,,,4,1.63
14:42:00,event,,,5,1.64
14:42:00,event,,,6,1.57
14:42:00,event,,,7,1.56
14:42:00,event,,,8,1.99
14:42:00,event,,,9,1.59
14:42:00,event,,,12,1.67
14:42:00,event,,,10,1.71
14:42:00,event,,,11,1.74
14:42:00,event,,,13,1.63
14:42:00,event,,,14,1.64
14:42:00,event,,,15,1.68
14:42:00,event,,,16,1.63
14:42:00,event,,,17,1.18
14:42:00,event,,,19,1.67
14:42:00,event,,,18,2.78
14:42:00,event,,,21,1.58
14:42:00,event,,,20,1.86
14:42:00,event,,,22,3.48
14:42:00,event,,,23,1.69
14:42:00,event,,,24,1.57
14:42:00,event,,,25,1.57
14:42:00,event,,,26,2.26
14:42:00,event,,,27,1.61
14:42:00,event,,,28,1.53
14:42:00,event,,,30,2.11
14:42:00,event,,,29,1.75
14:42:00,event,,,31,1.59
14:42:00,event,,,32,1.59
14:42:00,event,,,33,1.83
14:42:00,event,,,34,1.58
14:42:00,event,,,36,1.55
14:42:00,event,,,35,1.55
14:42:00,event,,,37,1.55
14:42:00,event,,,38,1.58
14:42:00,event,,,39,1.62
14:42:00,event,,,40,4.06
14:42:00,event,,,41,1.54
14:42:00,event,,,42,1.65
14:42:00,event,,,43,1.63
14:42:00,event,,,44,1.65
14:42:00,event,,,46,1.58
14:42:00,event,,,45,1.55
14:42:00,event,,,47,1.59
14:42:00,event,,,48,1.59
14:42:00,event,,,49,1.61
14:42:00,event,,,50,1.59
14:42:00,event,,,51,1.59
14:42:00,event,,,52,1.6
14:42:00,event,,,53,1.6
14:42:00,event,,,54,1.6
14:42:00,event,,,55,1.55
14:42:00,event,,,57,1.57
14:42:00,event,,,56,3.87
14:42:00,event,,,58,1.78
14:42:00,event,,,59,1.56
14:42:00,event,,,60,1.64
14:42:00,event,,,61,1.58
14:42:00,event,,,62,1.57
14:42:00,event,,,63,1.83
14:42:00,event,,,64,1.57
14:42:00,event,,,66,1.6
14:42:00,event,,,65,1.54
14:42:00,event,,,67,1.86
14:42:00,event,,,68,1.55
14:42:00,event,,,69,1.61
14:42:00,event,,,70,1.63
14:42:00,event,,,71,1.56
14:42:00,event,,,72,1.55
14:42:00,event,,,73,1.54
14:42:00,event,,,74,1.5
14:42:00,event,,,75,1.58
14:42:00,event,,,76,1.65
14:42:00,event,,,77,1.58
14:42:00,event,,,78,1.58
14:42:00,event,,,79,1.52
14:42:00,event,,,80,1.53
14:42:00,event,,,81,1.55
14:42:00,event,,,82,1.61
14:42:00,event,,,83,1.6
14:42:01,event,,,84,1.56
14:42:01,event,,,85,1.54
14:42:01,event,,,86,1.56
14:42:01,event,,,87,1.61
14:42:01,event,,,88,1.59
14:42:01,event,,,89,1.55
14:42:01,event,,,90,1.61
14:42:01,event,,,91,1.6
14:42:01,event,,,92,1.59
14:42:01,event,,,93,1.62
14:42:01,event,,,94,1.59
14:42:01,event,,,95,1.56
14:42:01,event,,,96,1.52
14:42:01,event,,,97,1.16
14:42:01,event,,,98,1.68
14:42:01,event,,,99,1.57
14:43:00,event,,,1,4.07
14:43:00,event,,,2,2.46
14:43:00,event,,,0,1.67
14:43:00,event,,,4,1.56
14:43:00,event,,,3,1.61
14:43:00,event,,,5,1.57
14:43:00,event,,,6,1.85
14:43:00,event,,,7,1.8
14:43:00,event,,,8,2.79
14:43:00,event,,,9,1.75
14:43:00,event,,,10,1.58
14:43:00,event,,,11,1.51
14:43:00,event,,,12,1.54
14:43:00,event,,,13,1.61
14:43:00,event,,,14,1.6
14:43:00,event,,,15,1.55
14:43:00,event,,,16,1.56
14:43:00,event,,,17,1.56
14:43:00,event,,,18,1.57
14:43:00,event,,,19,1.6
14:43:00,event,,,20,1.56
14:43:00,event,,,21,1.52
14:43:00,event,,,22,1.61
14:43:00,event,,,23,1.58
14:43:00,event,,,24,1.9
14:43:00,event,,,25,1.62
14:43:00,event,,,26,1.54
14:43:00,event,,,27,1.57
14:43:00,event,,,28,1.56
14:43:00,event,,,29,1.6
14:43:00,event,,,30,1.51
14:43:00,event,,,31,1.6
14:43:00,event,,,33,1.59
14:43:00,event,,,32,1.58
14:43:00,event,,,34,1.53
14:43:00,event,,,35,1.54
14:43:00,event,,,36,2.88
14:43:00,event,,,37,1.59
14:43:00,event,,,38,1.59
14:43:00,event,,,39,1.86
14:43:00,event,,,40,1.55
14:43:00,event,,,41,1.58
14:43:00,event,,,42,1.7
14:43:00,event,,,43,1.45
14:43:00,event,,,44,1.54
14:43:00,event,,,45,1.53
14:43:00,event,,,46,1.56
14:43:00,event,,,47,1.54
14:43:00,event,,,48,1.48
14:43:00,event,,,49,1.58
14:43:00,event,,,50,1.57
14:43:00,event,,,52,1.63
14:43:00,event,,,51,1.53
14:43:00,event,,,53,1.6
14:43:00,event,,,55,1.58
14:43:00,event,,,54,1.55
14:43:00,event,,,56,1.57
14:43:01,event,,,57,1.56
14:43:01,event,,,58,1.53
14:43:01,event,,,60,1.6
14:43:01,event,,,59,1.52
14:43:01,event,,,61,1.59
14:43:01,event,,,62,1.62
14:43:01,event,,,63,1.56
14:43:01,event,,,64,1.59
14:43:01,event,,,65,1.6
14:43:01,event,,,66,1.86
14:43:01,event,,,67,2.89
14:43:01,event,,,68,1.55
14:43:01,event,,,69,1.92
14:43:01,event,,,70,1.64
14:43:01,event,,,71,1.56
14:43:01,event,,,72,1.62
14:43:01,event,,,73,1.47
14:43:01,event,,,74,1.65
14:43:01,event,,,75,1.64
14:43:02,event,,,76,1.66
14:43:02,event,,,77,1.62
14:43:02,event,,,78,1.62
14:43:02,event,,,80,1.8
14:43:02,event,,,79,1.62
14:43:02,event,,,81,1.58
14:43:02,event,,,82,1.9
14:43:02,event,,,83,1.62
14:43:02,event,,,84,1.58
14:43:02,event,,,85,1.59
14:43:02,event,,,86,1.69
14:43:02,event,,,87,1.31
14:43:02,event,,,88,1.62
14:43:03,event,,,89,1.68
14:43:03,event,,,90,1.54
14:43:03,event,,,91,1.67
14:43:03,event,,,92,1.67
14:43:03,event,,,93,1.59
14:43:03,event,,,94,1.56
14:43:03,event,,,95,1.63
14:43:03,event,,,96,1.63
14:43:03,event,,,97,1.7
14:43:03,event,,,98,1.6
14:43:03,event,,,99,1.52
14:44:00,event,,,0,1.57
14:44:00,event,,,1,2.17
14:44:00,event,,,3,2.62
14:44:00,event,,,4,1.57
14:44:00,event,,,2,1.54
14:44:00,event,,,5,1.29
14:44:00,event,,,6,1.56
14:44:00,event,,,7,1.47
14:44:00,event,,,8,2.11
14:44:00,event,,,9,1.76
14:44:00,event,,,10,1.61
14:44:00,event,,,11,1.61
14:44:00,event,,,12,1.56
14:44:00,event,,,13,1.59
14:44:00,event,,,14,1.54
14:44:00,event,,,15,1.55
14:44:00,event,,,16,1.5
14:44:00,event,,,17,1.56
14:44:00,event,,,18,1.58
14:44:00,event,,,19,1.61
14:44:00,event,,,20,1.55
14:44:00,event,,,21,1.64
14:44:00,event,,,22,1.62
14:44:00,event,,,23,1.57
14:44:00,event,,,24,1.53
14:44:00,event,,,26,1.51
14:44:00,event,,,25,1.58
14:44:00,event,,,27,1.53
14:44:00,event,,,28,1.55
14:44:00,event,,,30,1.58
14:44:00,event,,,29,1.53
14:44:00,event,,,31,1.66
14:44:00,event,,,32,1.57
14:44:00,event,,,33,1.64
14:44:01,event,,,34,1.63
14:44:01,event,,,35,1.63
14:44:01,event,,,36,1.56
14:44:01,event,,,37,1.64
14:44:01,event,,,39,1.55
14:44:01,event,,,38,1.53
14:44:01,event,,,40,1.57
14:44:01,event,,,41,1.64
14:44:01,event,,,42,1.51
14:44:01,event,,,43,1.58
14:44:01,event,,,45,1.57
14:44:01,event,,,44,1.56
14:44:01,event,,,46,1.61
14:44:01,event,,,47,1.59
14:44:01,event,,,48,1.6
14:44:01,event,,,50,1.4
14:44:01,event,,,49,1.64
14:44:01,event,,,51,1.63
14:44:01,event,,,53,1.52
14:44:01,event,,,52,1.53
14:44:01,event,,,54,1.64
14:44:01,event,,,55,1.77
14:44:01,event,,,56,1.6
14:44:01,event,,,57,1.64
14:44:02,event,,,58,1.61
14:44:02,event,,,59,1.67
14:44:02,event,,,60,1.64
14:44:02,event,,,61,1.67
14:44:02,event,,,62,1.61
14:44:02,event,,,63,1.62
14:44:02,event,,,64,1.63
14:44:02,event,,,65,1.59
14:44:02,event,,,66,1.65
14:44:02,event,,,67,1.59
14:44:02,event,,,68,1.57
14:44:02,event,,,69,1.65
14:44:02,event,,,70,1.63
14:44:02,event,,,71,1.6
14:44:02,event,,,72,1.58
14:44:02,event,,,73,1.6
14:44:02,event,,,74,1.59
14:44:03,event,,,75,1.54
14:44:03,event,,,76,1.57
14:44:03,event,,,77,1.71
14:44:03,event,,,78,1.7
14:44:03,event,,,79,1.56
14:44:03,event,,,80,1.63
14:44:03,event,,,81,1.55
14:44:03,event,,,82,1.74
14:44:03,event,,,83,1.6
14:44:03,event,,,84,1.62
14:44:03,event,,,85,1.75
14:44:03,event,,,86,1.64
14:44:03,event,,,87,1.62
14:44:03,event,,,88,1.61
14:44:04,event,,,89,1.61
14:44:04,event,,,90,1.6
14:44:04,event,,,91,1.63
14:44:04,event,,,92,1.69
14:44:04,event,,,93,1.54
14:44:04,event,,,94,1.66
14:44:04,event,,,95,1.61
14:44:04,event,,,96,1.76
14:44:04,event,,,97,1.59
14:44:04,event,,,98,1.58
14:44:05,event,,,99,1.61
14:45:00,event,,,1,1.57
14:45:00,event,,,0,4.81
14:45:00,event,,,3,2.62
14:45:00,event,,,4,2.45
14:45:00,event,,,2,4.06
14:45:00,event,,,6,1.41
14:45:00,event,,,5,1.03
14:45:00,event,,,7,1.77
14:45:00,event,,,8,1.56
14:45:00,event,,,9,1.53
14:45:00,event,,,10,1.52
14:45:00,event,,,11,1.62
14:45:00,event,,,12,1.55
14:45:00,event,,,13,1.59
14:45:00,event,,,14,1.56
14:45:00,event,,,15,1.58
14:45:00,event,,,16,1.53
14:45:00,event,,,17,1.58
14:45:00,event,,,18,1.58
14:45:00,event,,,19,1.56
14:45:00,event,,,20,1.52
14:45:00,event,,,21,1.68
14:45:00,event,,,22,1.55
14:45:00,event,,,23,1.54
14:45:00,event,,,24,1.59
14:45:00,event,,,25,1.52
14:45:00,event,,,26,1.53
14:45:00,event,,,27,1.61
14:45:00,event,,,28,1.57
14:45:00,event,,,29,1.56
14:45:00,event,,,30,1.58
14:45:01,event,,,31,1.57
14:45:01,event,,,32,1.56
14:45:01,event,,,33,1.55
14:45:01,event,,,34,1.61
14:45:01,event,,,35,1.52
14:45:01,event,,,36,1.57
14:45:01,event,,,38,1.59
14:45:01,event,,,37,1.55
14:45:01,event,,,39,1.51
14:45:01,event,,,40,1.58
14:45:01,event,,,41,1.52
14:45:01,event,,,42,1.57
14:45:01,event,,,43,1.6
14:45:01,event,,,44,1.62
14:45:01,event,,,45,1.53
14:45:01,event,,,46,1.63
14:45:01,event,,,47,1.65
14:45:01,event,,,48,1.55
14:45:01,event,,,49,1.79
14:45:01,event,,,50,1.64
14:45:02,event,,,51,1.54
14:45:02,event,,,53,1.58
14:45:02,event,,,52,1.53
14:45:02,event,,,54,1.67
14:45:02,event,,,55,1.54
14:45:02,event,,,56,1.5
14:45:02,event,,,57,1.53
14:45:02,event,,,59,1.58
14:45:02,event,,,58,1.54
14:45:02,event,,,60,1.65
14:45:02,event,,,61,1.65
14:45:02,event,,,62,1.62
14:45:02,event,,,63,1.55
14:45:02,event,,,64,4.18
14:45:02,event,,,65,1.6
14:45:02,event,,,66,1.63
14:45:03,event,,,67,1.6
14:45:03,event,,,68,1.62
14:45:03,event,,,69,1.69
14:45:03,event,,,70,1.59
14:45:03,event,,,71,1.64
14:45:03,event,,,72,1.63
14:45:03,event,,,73,1.61
14:45:03,event,,,74,1.56
14:45:03,event,,,75,1.65
14:45:03,event,,,76,1.66
14:45:03,event,,,77,1.51
14:45:04,event,,,78,1.61
14:45:04,event,,,79,1.72
14:45:04,event,,,80,1.58
14:45:04,event,,,81,1.7
14:45:04,event,,,82,1.62
14:45:04,event,,,83,1.52
14:45:04,event,,,84,1.59
14:45:04,event,,,85,1.58
14:45:05,event,,,86,1.64
14:45:05,event,,,87,1.66
14:45:05,event,,,88,1.59
14:45:05,event,,,89,1.64
14:45:05,event,,,90,1.65
14:45:05,event,,,91,1.59
14:45:05,event,,,92,1.58
14:45:05,event,,,93,1.69
14:45:05,event,,,94,1.68
14:45:06,event,,,95,1.68
14:45:06,event,,,96,2.0
14:45:06,event,,,97,1.62
14:45:06,event,,,98,1.65
14:45:06,event,,,99,1.73
14:46:00,event,,,0,1.17
14:46:00,event,,,2,3.74
14:46:00,event,,,1,3.63
14:46:00,event,,,3,1.72
14:46:00,event,,,4,1.69
14:46:00,event,,,5,1.53
14:46:00,event,,,6,1.57
14:46:00,event,,,7,2.29
14:46:00,event,,,8,4.06
14:46:00,event,,,9,1.81
14:46:00,event,,,10,1.56
14:46:00,event,,,11,1.6
14:46:00,event,,,12,1.55
14:46:01,event,,,13,1.61
14:46:01,event,,,14,1.52
14:46:01,event,,,15,1.57
14:46:01,event,,,16,1.56
14:46:01,event,,,17,1.54
14:46:01,event,,,18,1.57
14:46:01,event,,,19,1.58
14:46:01,event,,,20,1.54
14:46:01,event,,,21,1.57
14:46:01,event,,,22,1.58
14:46:01,event,,,23,1.59
14:46:01,event,,,24,1.58
14:46:01,event,,,25,1.66
14:46:01,event,,,26,1.53
14:46:01,event,,,27,1.59
14:46:01,event,,,28,1.52
14:46:01,event,,,29,1.52
14:46:01,event,,,30,1.59
14:46:01,event,,,31,1.57
14:46:01,event,,,32,1.58
14:46:01,event,,,33,1.53
14:46:01,event,,,34,1.62
14:46:01,event,,,35,1.59
14:46:01,event,,,36,1.62
14:46:02,event,,,37,1.7
14:46:02,event,,,38,1.61
14:46:02,event,,,39,1.61
14:46:02,event,,,40,1.88
14:46:02,event,,,41,1.61
14:46:02,event,,,42,3.01
14:46:02,event,,,43,1.63
14:46:02,event,,,45,1.58
14:46:02,event,,,44,1.53
14:46:02,event,,,46,1.53
14:46:02,event,,,47,1.57
14:46:02,event,,,48,1.59
14:46:02,event,,,49,1.6
14:46:02,event,,,50,1.66
14:46:02,event,,,51,1.59
14:46:02,event,,,52,1.65
14:46:03,event,,,53,1.52
14:46:03,event,,,54,1.65
14:46:03,event,,,55,1.52
14:46:03,event,,,56,1.58
14:46:03,event,,,57,1.8
14:46:03,event,,,58,1.65
14:46:03,event,,,59,1.7
14:46:03,event,,,60,1.69
14:46:03,event,,,61,1.5
14:46:03,event,,,62,1.51
14:46:03,event,,,63,1.67
14:46:04,event,,,65,1.64
14:46:04,event,,,64,1.91
14:46:04,event,,,66,1.61
14:46:04,event,,,67,1.63
14:46:04,event,,,68,1.71
14:46:04,event,,,69,1.66
14:46:04,event,,,70,1.66
14:46:04,event,,,71,1.62
14:46:05,event,,,72,1.57
14:46:05,event,,,73,1.65
14:46:05,event,,,74,1.56
14:46:05,event,,,75,1.59
14:46:05,event,,,76,1.66
14:46:05,event,,,77,1.06
14:46:05,event,,,78,1.84
14:46:05,event,,,79,1.57
14:46:06,event,,,81,1.62
14:46:06,event,,,80,1.68
14:46:06,event,,,82,1.66
14:46:06,event,,,83,1.55
14:46:06,event,,,84,1.63
14:46:06,event,,,85,1.55
14:46:06,event,,,86,1.58
14:46:07,event,,,87,1.65
14:46:07,event,,,88,1.62
14:46:07,event,,,89,3.34
14:46:07,event,,,90,1.56
14:46:07,event,,,91,1.59
14:46:07,event,,,92,1.59
14:46:07,event,,,93,1.58
14:46:08,event,,,94,1.65
14:46:08,event,,,95,1.57
14:46:08,event,,,96,1.58
14:46:08,event,,,97,1.63
14:46:08,event,,,98,2.76
14:46:08,event,,,99,1.8
14:47:04,event,,,0,1.53
14:47:04,event,,,1,1.61
14:47:04,event,,,2,1.56
14:47:04,event,,,3,1.79
14:47:04,event,,,4,1.59
14:47:04,event,,,5,1.52
14:47:04,event,,,6,1.55
14:47:04,event,,,7,1.57
14:47:04,event,,,8,1.56
14:47:04,event,,,9,1.57
14:47:04,event,,,10,1.58
14:47:04,event,,,11,1.51
14:47:04,event,,,12,1.63
14:47:04,event,,,13,1.58
14:47:04,event,,,14,1.61
14:47:05,event,,,15,1.57
14:47:05,event,,,16,1.51
14:47:05,event,,,17,1.59
14:47:05,event,,,18,1.55
14:47:05,event,,,19,1.12
14:47:05,event,,,20,1.69
14:47:05,event,,,21,1.6
14:47:05,event,,,22,1.62
14:47:05,event,,,23,1.77
14:47:05,event,,,24,1.58
14:47:05,event,,,25,1.55
14:47:05,event,,,26,1.58
14:47:05,event,,,27,1.62
14:47:05,event,,,28,1.73
14:47:05,event,,,29,1.64
14:47:05,event,,,30,1.59
14:47:05,event,,,31,1.64
14:47:05,event,,,32,1.61
14:47:05,event,,,33,1.6
14:47:06,event,,,34,1.63
14:47:06,event,,,35,1.68
14:47:06,event,,,36,1.57
14:47:06,event,,,37,1.7
14:47:06,event,,,38,1.56
14:47:06,event,,,39,1.63
14:47:06,event,,,40,1.65
14:47:06,event,,,41,1.6
14:47:06,event,,,42,1.69
14:47:06,event,,,43,1.3
14:47:06,event,,,44,1.7
14:47:06,event,,,45,1.62
14:47:06,event,,,46,1.62
14:47:06,event,,,47,1.65
14:47:07,event,,,48,1.69
14:47:07,event,,,49,1.7
14:47:07,event,,,50,1.6
14:47:07,event,,,51,1.59
14:47:07,event,,,52,1.45
14:47:07,event,,,53,1.54
14:47:07,event,,,54,1.6
14:47:07,event,,,55,1.3
14:47:07,event,,,56,1.61
14:47:07,event,,,57,1.73
14:47:08,event,,,58,1.69
14:47:08,event,,,60,1.65
14:47:08,event,,,59,1.7
14:47:08,event,,,61,1.78
14:47:08,event,,,62,1.76
14:47:08,event,,,63,1.67
14:47:08,event,,,64,1.62
14:47:08,event,,,65,1.64
14:47:09,event,,,67,1.7
14:47:09,event,,,66,1.6
14:47:09,event,,,68,2.0
14:47:09,event,,,69,1.94
14:47:09,event,,,70,1.68
14:47:09,event,,,71,1.6
14:47:09,event,,,72,1.85
14:47:09,event,,,73,1.75
14:47:10,event,,,74,1.65
14:47:10,event,,,75,1.58
14:47:10,event,,,76,1.6
14:47:10,event,,,77,1.65
14:47:10,event,,,78,1.52
14:47:10,event,,,79,1.8
14:47:10,event,,,80,1.57
14:47:11,event,,,81,1.64
14:47:11,event,,,82,1.89
14:47:11,event,,,83,1.69
14:47:11,event,,,84,1.64
14:47:11,event,,,85,1.66
14:47:11,event,,,86,1.54
14:47:12,event,,,87,1.81
14:47:12,event,,,88,1.64
14:47:12,event,,,89,1.66
14:47:12,event,,,90,1.6
14:47:12,event,,,91,1.67
14:47:12,event,,,92,1.69
14:47:12,event,,,93,1.64
14:47:13,event,,,94,1.69
14:47:13,event,,,95,1.74
14:47:13,event,,,96,1.72
14:47:14,event,,,97,1.63
14:47:14,event,,,98,1.61
14:47:14,event,,,99,2.02
14:48:34,event,,,0,1.6
14:48:34,event,,,1,1.55
14:48:34,event,,,2,1.57
14:48:34,event,,,3,1.55
14:48:34,event,,,4,1.75
14:48:34,event,,,5,1.6
14:48:34,event,,,6,1.72
14:48:34,event,,,7,1.54
14:48:34,event,,,8,1.6
14:48:34,event,,,9,1.56
14:48:34,event,,,10,1.51
14:48:34,event,,,11,1.57
14:48:34,event,,,12,1.55
14:48:34,event,,,13,1.63
14:48:34,event,,,14,1.65
14:48:34,event,,,15,1.62
14:48:34,event,,,16,1.55
14:48:34,event,,,17,1.59
14:48:34,event,,,18,1.75
14:48:34,event,,,19,1.63
14:48:34,event,,,20,1.49
14:48:34,event,,,21,1.65
14:48:34,event,,,22,1.55
14:48:35,event,,,23,1.57
14:48:35,event,,,24,1.58
14:48:35,event,,,25,1.67
14:48:35,event,,,26,1.66
14:48:35,event,,,27,1.62
14:48:35,event,,,28,1.65
14:48:35,event,,,29,1.58
14:48:35,event,,,30,1.55
14:48:35,event,,,31,1.73
14:48:35,event,,,32,1.78
14:48:35,event,,,33,1.68
14:48:35,event,,,34,1.69
14:48:35,event,,,35,2.5
14:48:35,event,,,36,1.83
14:48:35,event,,,37,1.58
14:48:36,event,,,38,1.58
14:48:36,event,,,39,3.11
14:48:36,event,,,40,1.63
14:48:36,event,,,41,1.62
14:48:36,event,,,42,1.59
14:48:36,event,,,43,1.61
14:48:36,event,,,44,1.57
14:48:36,event,,,45,1.63
14:48:36,event,,,46,1.58
14:48:37,event,,,47,1.63
14:48:37,event,,,48,1.7
14:48:37,event,,,49,1.69
14:48:37,event,,,50,1.62
14:48:37,event,,,51,1.61
14:48:37,event,,,52,4.0
14:48:37,event,,,53,1.6
14:48:37,event,,,54,1.59
14:48:37,event,,,55,1.63
14:48:38,event,,,56,1.75
14:48:38,event,,,58,1.63
14:48:38,event,,,57,1.68
14:48:38,event,,,59,1.74
14:48:38,event,,,60,4.02
14:48:38,event,,,61,1.77
14:48:38,event,,,62,3.6
14:48:38,event,,,63,1.64
14:48:38,event,,,64,1.55
14:48:39,event,,,65,1.7
14:48:39,event,,,66,2.86
14:48:39,event,,,67,1.66
14:48:39,event,,,68,1.2
14:48:39,event,,,69,1.97
14:48:39,event,,,70,1.71
14:48:40,event,,,71,1.6
14:48:40,event,,,72,1.58
14:48:40,event,,,73,1.72
14:48:40,event,,,74,1.67
14:48:40,event,,,75,1.75
14:48:40,event,,,76,1.96
14:48:41,event,,,77,1.63
14:48:41,event,,,78,1.72
14:48:41,event,,,79,1.63
14:48:41,event,,,80,1.58
14:48:41,event,,,81,1.65
14:48:42,event,,,82,1.59
14:48:42,event,,,83,1.64
14:48:42,event,,,84,1.72
14:48:42,event,,,85,1.6
14:48:42,event,,,86,1.65
14:48:42,event,,,87,1.49
14:48:43,event,,,88,1.65
14:48:43,event,,,89,1.68
14:48:43,event,,,90,1.63
14:48:43,event,,,91,1.61
14:48:44,event,,,92,1.6
14:48:44,event,,,93,1.67
14:48:44,event,,,94,1.67
14:48:44,event,,,95,1.67
14:48:45,event,,,96,1.57
14:48:45,event,,,97,1.65
14:48:45,event,,,98,1.58
14:48:45,event,,,99,1.98
14:50:35,event,,,0,1.59
14:50:35,event,,,1,1.56
14:50:35,event,,,2,1.58
14:50:35,event,,,3,1.58
14:50:35,event,,,4,1.58
14:50:35,event,,,5,1.56
14:50:35,event,,,6,1.56
14:50:35,event,,,7,1.56
14:50:35,event,,,8,1.55
14:50:35,event,,,9,1.53
14:50:36,event,,,10,1.58
14:50:36,event,,,11,1.53
14:50:36,event,,,12,1.65
14:50:36,event,,,13,1.62
14:50:36,event,,,14,1.56
14:50:36,event,,,15,1.6
14:50:36,event,,,16,1.66
14:50:36,event,,,17,1.53
14:50:36,event,,,18,1.7
14:50:36,event,,,19,1.6
14:50:36,event,,,20,1.61
14:50:36,event,,,21,1.66
14:50:36,event,,,22,1.7
14:50:36,event,,,23,1.78
14:50:36,event,,,24,1.61
14:50:36,event,,,25,1.62
14:50:36,event,,,26,1.53
14:50:36,event,,,27,1.62
14:50:37,event,,,28,1.7
14:50:37,event,,,29,1.62
14:50:37,event,,,30,1.89
14:50:37,event,,,31,1.7
14:50:37,event,,,32,1.61
14:50:37,event,,,33,1.57
14:50:37,event,,,34,1.79
14:50:37,event,,,35,1.62
14:50:37,event,,,36,1.95
14:50:37,event,,,37,1.59
14:50:37,event,,,38,1.63
14:50:37,event,,,39,1.72
14:50:38,event,,,41,1.78
14:50:38,event,,,40,1.59
14:50:38,event,,,42,1.6
14:50:38,event,,,43,1.6
14:50:38,event,,,44,1.61
14:50:38,event,,,45,1.9
14:50:38,event,,,46,1.62
14:50:38,event,,,47,1.73
14:50:38,event,,,48,1.73
14:50:39,event,,,49,1.63
14:50:39,event,,,50,1.67
14:50:39,event,,,51,1.67
14:50:39,event,,,52,1.64
14:50:39,event,,,53,1.9
14:50:39,event,,,54,1.68
14:50:40,event,,,55,1.67
14:50:40,event,,,56,1.66
14:50:40,event,,,57,1.78
14:50:40,event,,,58,1.59
14:50:40,event,,,59,1.68
14:50:40,event,,,60,1.62
14:50:40,event,,,61,1.58
14:50:41,event,,,62,1.63
14:50:41,event,,,63,1.94
14:50:41,event,,,64,1.86
14:50:41,event,,,65,1.8
14:50:41,event,,,66,1.71
14:50:41,event,,,67,1.62
14:50:42,event,,,68,2.02
14:50:42,event,,,69,1.63
14:50:42,event,,,70,1.6
14:50:42,event,,,71,1.57
14:50:42,event,,,72,1.69
14:50:42,event,,,73,1.54
14:50:43,event,,,74,1.63
14:50:43,event,,,75,1.55
14:50:43,event,,,76,1.8
14:50:43,event,,,77,1.69
14:50:43,event,,,78,1.61
14:50:43,event,,,79,1.53
14:50:44,event,,,80,1.59
14:50:44,event,,,81,1.61
14:50:44,event,,,82,2.05
14:50:45,event,,,83,1.62
14:50:45,event,,,84,1.64
14:50:45,event,,,85,1.54
14:50:45,event,,,86,1.63
14:50:45,event,,,87,1.74
14:50:46,event,,,88,1.67
14:50:46,event,,,89,1.58
14:50:46,event,,,90,1.8
14:50:46,event,,,91,1.59
14:50:46,event,,,92,1.58
14:50:47,event,,,93,1.56
14:50:47,event,,,94,1.65
14:50:47,event,,,95,1.68
14:50:47,event,,,96,1.58
14:50:48,event,,,97,1.52
14:50:48,event,,,98,1.64
14:50:48,event,,,99,1.89
14:53:12,event,,,0,1.61
14:53:12,event,,,1,1.59
14:53:12,event,,,2,1.55
14:53:12,event,,,3,1.52
14:53:12,event,,,4,1.54
14:53:12,event,,,5,1.57
14:53:12,event,,,6,1.59
14:53:12,event,,,7,1.57
14:53:12,event,,,8,1.55
14:53:12,event,,,9,1.6
14:53:12,event,,,10,1.62
14:53:12,event,,,11,1.56
14:53:12,event,,,12,1.57
14:53:12,event,,,13,1.59
14:53:12,event,,,14,1.64
14:53:12,event,,,15,1.71
14:53:12,event,,,16,1.6
14:53:12,event,,,17,1.65
14:53:12,event,,,18,1.7
14:53:13,event,,,19,1.63
14:53:13,event,,,20,1.73
14:53:13,event,,,21,1.58
14:53:13,event,,,22,1.65
14:53:13,event,,,23,1.57
14:53:13,event,,,24,1.67
14:53:13,event,,,25,1.64
14:53:13,event,,,26,1.51
14:53:13,event,,,27,2.36
14:53:13,event,,,28,1.04
14:53:13,event,,,29,1.63
14:53:13,event,,,30,1.74
14:53:14,event,,,31,1.62
14:53:14,event,,,32,1.62
14:53:14,event,,,33,1.74
14:53:14,event,,,34,1.67
14:53:14,event,,,35,1.61
14:53:14,event,,,36,1.78
14:53:14,event,,,37,1.64
14:53:14,event,,,38,1.67
14:53:14,event,,,39,1.8
14:53:15,event,,,40,1.68
14:53:15,event,,,41,1.67
14:53:15,event,,,42,1.64
14:53:15,event,,,44,1.77
14:53:15,event,,,43,1.65
14:53:15,event,,,45,1.65
14:53:15,event,,,46,1.69
14:53:15,event,,,47,1.68
14:53:16,event,,,48,1.54
14:53:16,event,,,49,1.83
14:53:16,event,,,50,1.61
14:53:16,event,,,51,1.64
14:53:16,event,,,52,1.89
14:53:16,event,,,53,1.56
14:53:17,event,,,54,1.72
14:53:17,event,,,55,1.52
14:53:17,event,,,56,1.54
14:53:17,event,,,57,1.73
14:53:17,event,,,58,1.07
14:53:17,event,,,59,1.91
14:53:18,event,,,60,1.7
14:53:18,event,,,61,3.93
14:53:18,event,,,62,1.59
14:53:18,event,,,63,1.64
14:53:18,event,,,64,1.63
14:53:19,event,,,65,1.7
14:53:19,event,,,66,1.64
14:53:19,event,,,67,1.63
14:53:19,event,,,68,1.6
14:53:19,event,,,69,1.19
14:53:19,event,,,70,3.76
14:53:20,event,,,71,1.62
14:53:20,event,,,72,1.45
14:53:20,event,,,73,1.89
14:53:20,event,,,74,1.95
14:53:21,event,,,75,1.95
14:53:21,event,,,76,1.81
14:53:21,event,,,77,1.62
14:53:21,event,,,78,1.63
14:53:21,event,,,79,2.07
14:53:21,event,,,80,1.64
14:53:22,event,,,81,1.21
14:53:22,event,,,82,1.62
14:53:22,event,,,83,1.67
14:53:22,event,,,84,1.65
14:53:23,event,,,85,1.64
14:53:23,event,,,86,1.59
14:53:23,event,,,87,1.68
14:53:24,event,,,88,1.6
14:53:24,event,,,89,4.69
14:53:24,event,,,90,1.72
14:53:24,event,,,91,1.63
14:53:25,event,,,92,1.59
14:53:25,event,,,93,1.74
14:53:25,event,,,94,1.68
14:53:25,event,,,95,1.65
14:53:26,event,,,96,1.61
14:53:26,event,,,97,1.63
14:53:26,event,,,98,1.65
14:53:26,event,,,99,1.71
14:56:36,event,,,0,1.62
14:56:36,event,,,1,1.61
14:56:36,event,,,2,1.57
14:56:36,event,,,3,1.55
14:56:36,event,,,4,1.51
14:56:36,event,,,5,1.59
14:56:36,event,,,6,1.59
14:56:36,event,,,7,1.57
14:56:36,event,,,8,1.56
14:56:36,event,,,9,1.52
14:56:36,event,,,10,1.6
14:56:36,event,,,11,1.63
14:56:36,event,,,12,1.58
14:56:36,event,,,13,1.58
14:56:36,event,,,14,1.78
14:56:36,event,,,15,1.64
14:56:36,event,,,16,1.61
14:56:36,event,,,17,1.59
14:56:37,event,,,18,1.92
14:56:37,event,,,19,1.77
14:56:37,event,,,20,1.64
14:56:37,event,,,21,1.65
14:56:37,event,,,22,1.62
14:56:37,event,,,23,1.6
14:56:37,event,,,24,1.55
14:56:37,event,,,25,1.6
14:56:37,event,,,26,1.59
14:56:37,event,,,27,1.65
14:56:37,event,,,28,1.8
14:56:38,event,,,29,1.74
14:56:38,event,,,30,1.66
14:56:38,event,,,31,1.78
1 timestamp type batch_start emission_duration event_id event_duration
2 14:42:00 cron_start 2025-12-31
3 14:43:00 cron_start 2025-12-31
4 14:44:00 cron_start 2025-12-31
5 14:45:00 cron_start 2025-12-31
6 14:46:00 cron_start 2025-12-31
7 14:47:04 cron_start 2025-12-31
8 14:48:34 cron_start 2025-12-31
9 14:50:35 cron_start 2025-12-31
10 14:53:12 cron_start 2025-12-31
11 14:56:36 cron_start 2025-12-31
12 14:42:01 emission 1.135
13 14:43:03 emission 3.452
14 14:44:04 emission 4.422
15 14:45:06 emission 6.048
16 14:46:08 emission 7.921
17 14:47:14 emission 9.359
18 14:48:45 emission 11.004
19 14:50:48 emission 12.66
20 14:53:26 emission 14.074
21 14:42:00 event 0 3.16
22 14:42:00 event 1 4.01
23 14:42:00 event 2 3.25
24 14:42:00 event 3 1.57
25 14:42:00 event 4 1.63
26 14:42:00 event 5 1.64
27 14:42:00 event 6 1.57
28 14:42:00 event 7 1.56
29 14:42:00 event 8 1.99
30 14:42:00 event 9 1.59
31 14:42:00 event 12 1.67
32 14:42:00 event 10 1.71
33 14:42:00 event 11 1.74
34 14:42:00 event 13 1.63
35 14:42:00 event 14 1.64
36 14:42:00 event 15 1.68
37 14:42:00 event 16 1.63
38 14:42:00 event 17 1.18
39 14:42:00 event 19 1.67
40 14:42:00 event 18 2.78
41 14:42:00 event 21 1.58
42 14:42:00 event 20 1.86
43 14:42:00 event 22 3.48
44 14:42:00 event 23 1.69
45 14:42:00 event 24 1.57
46 14:42:00 event 25 1.57
47 14:42:00 event 26 2.26
48 14:42:00 event 27 1.61
49 14:42:00 event 28 1.53
50 14:42:00 event 30 2.11
51 14:42:00 event 29 1.75
52 14:42:00 event 31 1.59
53 14:42:00 event 32 1.59
54 14:42:00 event 33 1.83
55 14:42:00 event 34 1.58
56 14:42:00 event 36 1.55
57 14:42:00 event 35 1.55
58 14:42:00 event 37 1.55
59 14:42:00 event 38 1.58
60 14:42:00 event 39 1.62
61 14:42:00 event 40 4.06
62 14:42:00 event 41 1.54
63 14:42:00 event 42 1.65
64 14:42:00 event 43 1.63
65 14:42:00 event 44 1.65
66 14:42:00 event 46 1.58
67 14:42:00 event 45 1.55
68 14:42:00 event 47 1.59
69 14:42:00 event 48 1.59
70 14:42:00 event 49 1.61
71 14:42:00 event 50 1.59
72 14:42:00 event 51 1.59
73 14:42:00 event 52 1.6
74 14:42:00 event 53 1.6
75 14:42:00 event 54 1.6
76 14:42:00 event 55 1.55
77 14:42:00 event 57 1.57
78 14:42:00 event 56 3.87
79 14:42:00 event 58 1.78
80 14:42:00 event 59 1.56
81 14:42:00 event 60 1.64
82 14:42:00 event 61 1.58
83 14:42:00 event 62 1.57
84 14:42:00 event 63 1.83
85 14:42:00 event 64 1.57
86 14:42:00 event 66 1.6
87 14:42:00 event 65 1.54
88 14:42:00 event 67 1.86
89 14:42:00 event 68 1.55
90 14:42:00 event 69 1.61
91 14:42:00 event 70 1.63
92 14:42:00 event 71 1.56
93 14:42:00 event 72 1.55
94 14:42:00 event 73 1.54
95 14:42:00 event 74 1.5
96 14:42:00 event 75 1.58
97 14:42:00 event 76 1.65
98 14:42:00 event 77 1.58
99 14:42:00 event 78 1.58
100 14:42:00 event 79 1.52
101 14:42:00 event 80 1.53
102 14:42:00 event 81 1.55
103 14:42:00 event 82 1.61
104 14:42:00 event 83 1.6
105 14:42:01 event 84 1.56
106 14:42:01 event 85 1.54
107 14:42:01 event 86 1.56
108 14:42:01 event 87 1.61
109 14:42:01 event 88 1.59
110 14:42:01 event 89 1.55
111 14:42:01 event 90 1.61
112 14:42:01 event 91 1.6
113 14:42:01 event 92 1.59
114 14:42:01 event 93 1.62
115 14:42:01 event 94 1.59
116 14:42:01 event 95 1.56
117 14:42:01 event 96 1.52
118 14:42:01 event 97 1.16
119 14:42:01 event 98 1.68
120 14:42:01 event 99 1.57
121 14:43:00 event 1 4.07
122 14:43:00 event 2 2.46
123 14:43:00 event 0 1.67
124 14:43:00 event 4 1.56
125 14:43:00 event 3 1.61
126 14:43:00 event 5 1.57
127 14:43:00 event 6 1.85
128 14:43:00 event 7 1.8
129 14:43:00 event 8 2.79
130 14:43:00 event 9 1.75
131 14:43:00 event 10 1.58
132 14:43:00 event 11 1.51
133 14:43:00 event 12 1.54
134 14:43:00 event 13 1.61
135 14:43:00 event 14 1.6
136 14:43:00 event 15 1.55
137 14:43:00 event 16 1.56
138 14:43:00 event 17 1.56
139 14:43:00 event 18 1.57
140 14:43:00 event 19 1.6
141 14:43:00 event 20 1.56
142 14:43:00 event 21 1.52
143 14:43:00 event 22 1.61
144 14:43:00 event 23 1.58
145 14:43:00 event 24 1.9
146 14:43:00 event 25 1.62
147 14:43:00 event 26 1.54
148 14:43:00 event 27 1.57
149 14:43:00 event 28 1.56
150 14:43:00 event 29 1.6
151 14:43:00 event 30 1.51
152 14:43:00 event 31 1.6
153 14:43:00 event 33 1.59
154 14:43:00 event 32 1.58
155 14:43:00 event 34 1.53
156 14:43:00 event 35 1.54
157 14:43:00 event 36 2.88
158 14:43:00 event 37 1.59
159 14:43:00 event 38 1.59
160 14:43:00 event 39 1.86
161 14:43:00 event 40 1.55
162 14:43:00 event 41 1.58
163 14:43:00 event 42 1.7
164 14:43:00 event 43 1.45
165 14:43:00 event 44 1.54
166 14:43:00 event 45 1.53
167 14:43:00 event 46 1.56
168 14:43:00 event 47 1.54
169 14:43:00 event 48 1.48
170 14:43:00 event 49 1.58
171 14:43:00 event 50 1.57
172 14:43:00 event 52 1.63
173 14:43:00 event 51 1.53
174 14:43:00 event 53 1.6
175 14:43:00 event 55 1.58
176 14:43:00 event 54 1.55
177 14:43:00 event 56 1.57
178 14:43:01 event 57 1.56
179 14:43:01 event 58 1.53
180 14:43:01 event 60 1.6
181 14:43:01 event 59 1.52
182 14:43:01 event 61 1.59
183 14:43:01 event 62 1.62
184 14:43:01 event 63 1.56
185 14:43:01 event 64 1.59
186 14:43:01 event 65 1.6
187 14:43:01 event 66 1.86
188 14:43:01 event 67 2.89
189 14:43:01 event 68 1.55
190 14:43:01 event 69 1.92
191 14:43:01 event 70 1.64
192 14:43:01 event 71 1.56
193 14:43:01 event 72 1.62
194 14:43:01 event 73 1.47
195 14:43:01 event 74 1.65
196 14:43:01 event 75 1.64
197 14:43:02 event 76 1.66
198 14:43:02 event 77 1.62
199 14:43:02 event 78 1.62
200 14:43:02 event 80 1.8
201 14:43:02 event 79 1.62
202 14:43:02 event 81 1.58
203 14:43:02 event 82 1.9
204 14:43:02 event 83 1.62
205 14:43:02 event 84 1.58
206 14:43:02 event 85 1.59
207 14:43:02 event 86 1.69
208 14:43:02 event 87 1.31
209 14:43:02 event 88 1.62
210 14:43:03 event 89 1.68
211 14:43:03 event 90 1.54
212 14:43:03 event 91 1.67
213 14:43:03 event 92 1.67
214 14:43:03 event 93 1.59
215 14:43:03 event 94 1.56
216 14:43:03 event 95 1.63
217 14:43:03 event 96 1.63
218 14:43:03 event 97 1.7
219 14:43:03 event 98 1.6
220 14:43:03 event 99 1.52
221 14:44:00 event 0 1.57
222 14:44:00 event 1 2.17
223 14:44:00 event 3 2.62
224 14:44:00 event 4 1.57
225 14:44:00 event 2 1.54
226 14:44:00 event 5 1.29
227 14:44:00 event 6 1.56
228 14:44:00 event 7 1.47
229 14:44:00 event 8 2.11
230 14:44:00 event 9 1.76
231 14:44:00 event 10 1.61
232 14:44:00 event 11 1.61
233 14:44:00 event 12 1.56
234 14:44:00 event 13 1.59
235 14:44:00 event 14 1.54
236 14:44:00 event 15 1.55
237 14:44:00 event 16 1.5
238 14:44:00 event 17 1.56
239 14:44:00 event 18 1.58
240 14:44:00 event 19 1.61
241 14:44:00 event 20 1.55
242 14:44:00 event 21 1.64
243 14:44:00 event 22 1.62
244 14:44:00 event 23 1.57
245 14:44:00 event 24 1.53
246 14:44:00 event 26 1.51
247 14:44:00 event 25 1.58
248 14:44:00 event 27 1.53
249 14:44:00 event 28 1.55
250 14:44:00 event 30 1.58
251 14:44:00 event 29 1.53
252 14:44:00 event 31 1.66
253 14:44:00 event 32 1.57
254 14:44:00 event 33 1.64
255 14:44:01 event 34 1.63
256 14:44:01 event 35 1.63
257 14:44:01 event 36 1.56
258 14:44:01 event 37 1.64
259 14:44:01 event 39 1.55
260 14:44:01 event 38 1.53
261 14:44:01 event 40 1.57
262 14:44:01 event 41 1.64
263 14:44:01 event 42 1.51
264 14:44:01 event 43 1.58
265 14:44:01 event 45 1.57
266 14:44:01 event 44 1.56
267 14:44:01 event 46 1.61
268 14:44:01 event 47 1.59
269 14:44:01 event 48 1.6
270 14:44:01 event 50 1.4
271 14:44:01 event 49 1.64
272 14:44:01 event 51 1.63
273 14:44:01 event 53 1.52
274 14:44:01 event 52 1.53
275 14:44:01 event 54 1.64
276 14:44:01 event 55 1.77
277 14:44:01 event 56 1.6
278 14:44:01 event 57 1.64
279 14:44:02 event 58 1.61
280 14:44:02 event 59 1.67
281 14:44:02 event 60 1.64
282 14:44:02 event 61 1.67
283 14:44:02 event 62 1.61
284 14:44:02 event 63 1.62
285 14:44:02 event 64 1.63
286 14:44:02 event 65 1.59
287 14:44:02 event 66 1.65
288 14:44:02 event 67 1.59
289 14:44:02 event 68 1.57
290 14:44:02 event 69 1.65
291 14:44:02 event 70 1.63
292 14:44:02 event 71 1.6
293 14:44:02 event 72 1.58
294 14:44:02 event 73 1.6
295 14:44:02 event 74 1.59
296 14:44:03 event 75 1.54
297 14:44:03 event 76 1.57
298 14:44:03 event 77 1.71
299 14:44:03 event 78 1.7
300 14:44:03 event 79 1.56
301 14:44:03 event 80 1.63
302 14:44:03 event 81 1.55
303 14:44:03 event 82 1.74
304 14:44:03 event 83 1.6
305 14:44:03 event 84 1.62
306 14:44:03 event 85 1.75
307 14:44:03 event 86 1.64
308 14:44:03 event 87 1.62
309 14:44:03 event 88 1.61
310 14:44:04 event 89 1.61
311 14:44:04 event 90 1.6
312 14:44:04 event 91 1.63
313 14:44:04 event 92 1.69
314 14:44:04 event 93 1.54
315 14:44:04 event 94 1.66
316 14:44:04 event 95 1.61
317 14:44:04 event 96 1.76
318 14:44:04 event 97 1.59
319 14:44:04 event 98 1.58
320 14:44:05 event 99 1.61
321 14:45:00 event 1 1.57
322 14:45:00 event 0 4.81
323 14:45:00 event 3 2.62
324 14:45:00 event 4 2.45
325 14:45:00 event 2 4.06
326 14:45:00 event 6 1.41
327 14:45:00 event 5 1.03
328 14:45:00 event 7 1.77
329 14:45:00 event 8 1.56
330 14:45:00 event 9 1.53
331 14:45:00 event 10 1.52
332 14:45:00 event 11 1.62
333 14:45:00 event 12 1.55
334 14:45:00 event 13 1.59
335 14:45:00 event 14 1.56
336 14:45:00 event 15 1.58
337 14:45:00 event 16 1.53
338 14:45:00 event 17 1.58
339 14:45:00 event 18 1.58
340 14:45:00 event 19 1.56
341 14:45:00 event 20 1.52
342 14:45:00 event 21 1.68
343 14:45:00 event 22 1.55
344 14:45:00 event 23 1.54
345 14:45:00 event 24 1.59
346 14:45:00 event 25 1.52
347 14:45:00 event 26 1.53
348 14:45:00 event 27 1.61
349 14:45:00 event 28 1.57
350 14:45:00 event 29 1.56
351 14:45:00 event 30 1.58
352 14:45:01 event 31 1.57
353 14:45:01 event 32 1.56
354 14:45:01 event 33 1.55
355 14:45:01 event 34 1.61
356 14:45:01 event 35 1.52
357 14:45:01 event 36 1.57
358 14:45:01 event 38 1.59
359 14:45:01 event 37 1.55
360 14:45:01 event 39 1.51
361 14:45:01 event 40 1.58
362 14:45:01 event 41 1.52
363 14:45:01 event 42 1.57
364 14:45:01 event 43 1.6
365 14:45:01 event 44 1.62
366 14:45:01 event 45 1.53
367 14:45:01 event 46 1.63
368 14:45:01 event 47 1.65
369 14:45:01 event 48 1.55
370 14:45:01 event 49 1.79
371 14:45:01 event 50 1.64
372 14:45:02 event 51 1.54
373 14:45:02 event 53 1.58
374 14:45:02 event 52 1.53
375 14:45:02 event 54 1.67
376 14:45:02 event 55 1.54
377 14:45:02 event 56 1.5
378 14:45:02 event 57 1.53
379 14:45:02 event 59 1.58
380 14:45:02 event 58 1.54
381 14:45:02 event 60 1.65
382 14:45:02 event 61 1.65
383 14:45:02 event 62 1.62
384 14:45:02 event 63 1.55
385 14:45:02 event 64 4.18
386 14:45:02 event 65 1.6
387 14:45:02 event 66 1.63
388 14:45:03 event 67 1.6
389 14:45:03 event 68 1.62
390 14:45:03 event 69 1.69
391 14:45:03 event 70 1.59
392 14:45:03 event 71 1.64
393 14:45:03 event 72 1.63
394 14:45:03 event 73 1.61
395 14:45:03 event 74 1.56
396 14:45:03 event 75 1.65
397 14:45:03 event 76 1.66
398 14:45:03 event 77 1.51
399 14:45:04 event 78 1.61
400 14:45:04 event 79 1.72
401 14:45:04 event 80 1.58
402 14:45:04 event 81 1.7
403 14:45:04 event 82 1.62
404 14:45:04 event 83 1.52
405 14:45:04 event 84 1.59
406 14:45:04 event 85 1.58
407 14:45:05 event 86 1.64
408 14:45:05 event 87 1.66
409 14:45:05 event 88 1.59
410 14:45:05 event 89 1.64
411 14:45:05 event 90 1.65
412 14:45:05 event 91 1.59
413 14:45:05 event 92 1.58
414 14:45:05 event 93 1.69
415 14:45:05 event 94 1.68
416 14:45:06 event 95 1.68
417 14:45:06 event 96 2.0
418 14:45:06 event 97 1.62
419 14:45:06 event 98 1.65
420 14:45:06 event 99 1.73
421 14:46:00 event 0 1.17
422 14:46:00 event 2 3.74
423 14:46:00 event 1 3.63
424 14:46:00 event 3 1.72
425 14:46:00 event 4 1.69
426 14:46:00 event 5 1.53
427 14:46:00 event 6 1.57
428 14:46:00 event 7 2.29
429 14:46:00 event 8 4.06
430 14:46:00 event 9 1.81
431 14:46:00 event 10 1.56
432 14:46:00 event 11 1.6
433 14:46:00 event 12 1.55
434 14:46:01 event 13 1.61
435 14:46:01 event 14 1.52
436 14:46:01 event 15 1.57
437 14:46:01 event 16 1.56
438 14:46:01 event 17 1.54
439 14:46:01 event 18 1.57
440 14:46:01 event 19 1.58
441 14:46:01 event 20 1.54
442 14:46:01 event 21 1.57
443 14:46:01 event 22 1.58
444 14:46:01 event 23 1.59
445 14:46:01 event 24 1.58
446 14:46:01 event 25 1.66
447 14:46:01 event 26 1.53
448 14:46:01 event 27 1.59
449 14:46:01 event 28 1.52
450 14:46:01 event 29 1.52
451 14:46:01 event 30 1.59
452 14:46:01 event 31 1.57
453 14:46:01 event 32 1.58
454 14:46:01 event 33 1.53
455 14:46:01 event 34 1.62
456 14:46:01 event 35 1.59
457 14:46:01 event 36 1.62
458 14:46:02 event 37 1.7
459 14:46:02 event 38 1.61
460 14:46:02 event 39 1.61
461 14:46:02 event 40 1.88
462 14:46:02 event 41 1.61
463 14:46:02 event 42 3.01
464 14:46:02 event 43 1.63
465 14:46:02 event 45 1.58
466 14:46:02 event 44 1.53
467 14:46:02 event 46 1.53
468 14:46:02 event 47 1.57
469 14:46:02 event 48 1.59
470 14:46:02 event 49 1.6
471 14:46:02 event 50 1.66
472 14:46:02 event 51 1.59
473 14:46:02 event 52 1.65
474 14:46:03 event 53 1.52
475 14:46:03 event 54 1.65
476 14:46:03 event 55 1.52
477 14:46:03 event 56 1.58
478 14:46:03 event 57 1.8
479 14:46:03 event 58 1.65
480 14:46:03 event 59 1.7
481 14:46:03 event 60 1.69
482 14:46:03 event 61 1.5
483 14:46:03 event 62 1.51
484 14:46:03 event 63 1.67
485 14:46:04 event 65 1.64
486 14:46:04 event 64 1.91
487 14:46:04 event 66 1.61
488 14:46:04 event 67 1.63
489 14:46:04 event 68 1.71
490 14:46:04 event 69 1.66
491 14:46:04 event 70 1.66
492 14:46:04 event 71 1.62
493 14:46:05 event 72 1.57
494 14:46:05 event 73 1.65
495 14:46:05 event 74 1.56
496 14:46:05 event 75 1.59
497 14:46:05 event 76 1.66
498 14:46:05 event 77 1.06
499 14:46:05 event 78 1.84
500 14:46:05 event 79 1.57
501 14:46:06 event 81 1.62
502 14:46:06 event 80 1.68
503 14:46:06 event 82 1.66
504 14:46:06 event 83 1.55
505 14:46:06 event 84 1.63
506 14:46:06 event 85 1.55
507 14:46:06 event 86 1.58
508 14:46:07 event 87 1.65
509 14:46:07 event 88 1.62
510 14:46:07 event 89 3.34
511 14:46:07 event 90 1.56
512 14:46:07 event 91 1.59
513 14:46:07 event 92 1.59
514 14:46:07 event 93 1.58
515 14:46:08 event 94 1.65
516 14:46:08 event 95 1.57
517 14:46:08 event 96 1.58
518 14:46:08 event 97 1.63
519 14:46:08 event 98 2.76
520 14:46:08 event 99 1.8
521 14:47:04 event 0 1.53
522 14:47:04 event 1 1.61
523 14:47:04 event 2 1.56
524 14:47:04 event 3 1.79
525 14:47:04 event 4 1.59
526 14:47:04 event 5 1.52
527 14:47:04 event 6 1.55
528 14:47:04 event 7 1.57
529 14:47:04 event 8 1.56
530 14:47:04 event 9 1.57
531 14:47:04 event 10 1.58
532 14:47:04 event 11 1.51
533 14:47:04 event 12 1.63
534 14:47:04 event 13 1.58
535 14:47:04 event 14 1.61
536 14:47:05 event 15 1.57
537 14:47:05 event 16 1.51
538 14:47:05 event 17 1.59
539 14:47:05 event 18 1.55
540 14:47:05 event 19 1.12
541 14:47:05 event 20 1.69
542 14:47:05 event 21 1.6
543 14:47:05 event 22 1.62
544 14:47:05 event 23 1.77
545 14:47:05 event 24 1.58
546 14:47:05 event 25 1.55
547 14:47:05 event 26 1.58
548 14:47:05 event 27 1.62
549 14:47:05 event 28 1.73
550 14:47:05 event 29 1.64
551 14:47:05 event 30 1.59
552 14:47:05 event 31 1.64
553 14:47:05 event 32 1.61
554 14:47:05 event 33 1.6
555 14:47:06 event 34 1.63
556 14:47:06 event 35 1.68
557 14:47:06 event 36 1.57
558 14:47:06 event 37 1.7
559 14:47:06 event 38 1.56
560 14:47:06 event 39 1.63
561 14:47:06 event 40 1.65
562 14:47:06 event 41 1.6
563 14:47:06 event 42 1.69
564 14:47:06 event 43 1.3
565 14:47:06 event 44 1.7
566 14:47:06 event 45 1.62
567 14:47:06 event 46 1.62
568 14:47:06 event 47 1.65
569 14:47:07 event 48 1.69
570 14:47:07 event 49 1.7
571 14:47:07 event 50 1.6
572 14:47:07 event 51 1.59
573 14:47:07 event 52 1.45
574 14:47:07 event 53 1.54
575 14:47:07 event 54 1.6
576 14:47:07 event 55 1.3
577 14:47:07 event 56 1.61
578 14:47:07 event 57 1.73
579 14:47:08 event 58 1.69
580 14:47:08 event 60 1.65
581 14:47:08 event 59 1.7
582 14:47:08 event 61 1.78
583 14:47:08 event 62 1.76
584 14:47:08 event 63 1.67
585 14:47:08 event 64 1.62
586 14:47:08 event 65 1.64
587 14:47:09 event 67 1.7
588 14:47:09 event 66 1.6
589 14:47:09 event 68 2.0
590 14:47:09 event 69 1.94
591 14:47:09 event 70 1.68
592 14:47:09 event 71 1.6
593 14:47:09 event 72 1.85
594 14:47:09 event 73 1.75
595 14:47:10 event 74 1.65
596 14:47:10 event 75 1.58
597 14:47:10 event 76 1.6
598 14:47:10 event 77 1.65
599 14:47:10 event 78 1.52
600 14:47:10 event 79 1.8
601 14:47:10 event 80 1.57
602 14:47:11 event 81 1.64
603 14:47:11 event 82 1.89
604 14:47:11 event 83 1.69
605 14:47:11 event 84 1.64
606 14:47:11 event 85 1.66
607 14:47:11 event 86 1.54
608 14:47:12 event 87 1.81
609 14:47:12 event 88 1.64
610 14:47:12 event 89 1.66
611 14:47:12 event 90 1.6
612 14:47:12 event 91 1.67
613 14:47:12 event 92 1.69
614 14:47:12 event 93 1.64
615 14:47:13 event 94 1.69
616 14:47:13 event 95 1.74
617 14:47:13 event 96 1.72
618 14:47:14 event 97 1.63
619 14:47:14 event 98 1.61
620 14:47:14 event 99 2.02
621 14:48:34 event 0 1.6
622 14:48:34 event 1 1.55
623 14:48:34 event 2 1.57
624 14:48:34 event 3 1.55
625 14:48:34 event 4 1.75
626 14:48:34 event 5 1.6
627 14:48:34 event 6 1.72
628 14:48:34 event 7 1.54
629 14:48:34 event 8 1.6
630 14:48:34 event 9 1.56
631 14:48:34 event 10 1.51
632 14:48:34 event 11 1.57
633 14:48:34 event 12 1.55
634 14:48:34 event 13 1.63
635 14:48:34 event 14 1.65
636 14:48:34 event 15 1.62
637 14:48:34 event 16 1.55
638 14:48:34 event 17 1.59
639 14:48:34 event 18 1.75
640 14:48:34 event 19 1.63
641 14:48:34 event 20 1.49
642 14:48:34 event 21 1.65
643 14:48:34 event 22 1.55
644 14:48:35 event 23 1.57
645 14:48:35 event 24 1.58
646 14:48:35 event 25 1.67
647 14:48:35 event 26 1.66
648 14:48:35 event 27 1.62
649 14:48:35 event 28 1.65
650 14:48:35 event 29 1.58
651 14:48:35 event 30 1.55
652 14:48:35 event 31 1.73
653 14:48:35 event 32 1.78
654 14:48:35 event 33 1.68
655 14:48:35 event 34 1.69
656 14:48:35 event 35 2.5
657 14:48:35 event 36 1.83
658 14:48:35 event 37 1.58
659 14:48:36 event 38 1.58
660 14:48:36 event 39 3.11
661 14:48:36 event 40 1.63
662 14:48:36 event 41 1.62
663 14:48:36 event 42 1.59
664 14:48:36 event 43 1.61
665 14:48:36 event 44 1.57
666 14:48:36 event 45 1.63
667 14:48:36 event 46 1.58
668 14:48:37 event 47 1.63
669 14:48:37 event 48 1.7
670 14:48:37 event 49 1.69
671 14:48:37 event 50 1.62
672 14:48:37 event 51 1.61
673 14:48:37 event 52 4.0
674 14:48:37 event 53 1.6
675 14:48:37 event 54 1.59
676 14:48:37 event 55 1.63
677 14:48:38 event 56 1.75
678 14:48:38 event 58 1.63
679 14:48:38 event 57 1.68
680 14:48:38 event 59 1.74
681 14:48:38 event 60 4.02
682 14:48:38 event 61 1.77
683 14:48:38 event 62 3.6
684 14:48:38 event 63 1.64
685 14:48:38 event 64 1.55
686 14:48:39 event 65 1.7
687 14:48:39 event 66 2.86
688 14:48:39 event 67 1.66
689 14:48:39 event 68 1.2
690 14:48:39 event 69 1.97
691 14:48:39 event 70 1.71
692 14:48:40 event 71 1.6
693 14:48:40 event 72 1.58
694 14:48:40 event 73 1.72
695 14:48:40 event 74 1.67
696 14:48:40 event 75 1.75
697 14:48:40 event 76 1.96
698 14:48:41 event 77 1.63
699 14:48:41 event 78 1.72
700 14:48:41 event 79 1.63
701 14:48:41 event 80 1.58
702 14:48:41 event 81 1.65
703 14:48:42 event 82 1.59
704 14:48:42 event 83 1.64
705 14:48:42 event 84 1.72
706 14:48:42 event 85 1.6
707 14:48:42 event 86 1.65
708 14:48:42 event 87 1.49
709 14:48:43 event 88 1.65
710 14:48:43 event 89 1.68
711 14:48:43 event 90 1.63
712 14:48:43 event 91 1.61
713 14:48:44 event 92 1.6
714 14:48:44 event 93 1.67
715 14:48:44 event 94 1.67
716 14:48:44 event 95 1.67
717 14:48:45 event 96 1.57
718 14:48:45 event 97 1.65
719 14:48:45 event 98 1.58
720 14:48:45 event 99 1.98
721 14:50:35 event 0 1.59
722 14:50:35 event 1 1.56
723 14:50:35 event 2 1.58
724 14:50:35 event 3 1.58
725 14:50:35 event 4 1.58
726 14:50:35 event 5 1.56
727 14:50:35 event 6 1.56
728 14:50:35 event 7 1.56
729 14:50:35 event 8 1.55
730 14:50:35 event 9 1.53
731 14:50:36 event 10 1.58
732 14:50:36 event 11 1.53
733 14:50:36 event 12 1.65
734 14:50:36 event 13 1.62
735 14:50:36 event 14 1.56
736 14:50:36 event 15 1.6
737 14:50:36 event 16 1.66
738 14:50:36 event 17 1.53
739 14:50:36 event 18 1.7
740 14:50:36 event 19 1.6
741 14:50:36 event 20 1.61
742 14:50:36 event 21 1.66
743 14:50:36 event 22 1.7
744 14:50:36 event 23 1.78
745 14:50:36 event 24 1.61
746 14:50:36 event 25 1.62
747 14:50:36 event 26 1.53
748 14:50:36 event 27 1.62
749 14:50:37 event 28 1.7
750 14:50:37 event 29 1.62
751 14:50:37 event 30 1.89
752 14:50:37 event 31 1.7
753 14:50:37 event 32 1.61
754 14:50:37 event 33 1.57
755 14:50:37 event 34 1.79
756 14:50:37 event 35 1.62
757 14:50:37 event 36 1.95
758 14:50:37 event 37 1.59
759 14:50:37 event 38 1.63
760 14:50:37 event 39 1.72
761 14:50:38 event 41 1.78
762 14:50:38 event 40 1.59
763 14:50:38 event 42 1.6
764 14:50:38 event 43 1.6
765 14:50:38 event 44 1.61
766 14:50:38 event 45 1.9
767 14:50:38 event 46 1.62
768 14:50:38 event 47 1.73
769 14:50:38 event 48 1.73
770 14:50:39 event 49 1.63
771 14:50:39 event 50 1.67
772 14:50:39 event 51 1.67
773 14:50:39 event 52 1.64
774 14:50:39 event 53 1.9
775 14:50:39 event 54 1.68
776 14:50:40 event 55 1.67
777 14:50:40 event 56 1.66
778 14:50:40 event 57 1.78
779 14:50:40 event 58 1.59
780 14:50:40 event 59 1.68
781 14:50:40 event 60 1.62
782 14:50:40 event 61 1.58
783 14:50:41 event 62 1.63
784 14:50:41 event 63 1.94
785 14:50:41 event 64 1.86
786 14:50:41 event 65 1.8
787 14:50:41 event 66 1.71
788 14:50:41 event 67 1.62
789 14:50:42 event 68 2.02
790 14:50:42 event 69 1.63
791 14:50:42 event 70 1.6
792 14:50:42 event 71 1.57
793 14:50:42 event 72 1.69
794 14:50:42 event 73 1.54
795 14:50:43 event 74 1.63
796 14:50:43 event 75 1.55
797 14:50:43 event 76 1.8
798 14:50:43 event 77 1.69
799 14:50:43 event 78 1.61
800 14:50:43 event 79 1.53
801 14:50:44 event 80 1.59
802 14:50:44 event 81 1.61
803 14:50:44 event 82 2.05
804 14:50:45 event 83 1.62
805 14:50:45 event 84 1.64
806 14:50:45 event 85 1.54
807 14:50:45 event 86 1.63
808 14:50:45 event 87 1.74
809 14:50:46 event 88 1.67
810 14:50:46 event 89 1.58
811 14:50:46 event 90 1.8
812 14:50:46 event 91 1.59
813 14:50:46 event 92 1.58
814 14:50:47 event 93 1.56
815 14:50:47 event 94 1.65
816 14:50:47 event 95 1.68
817 14:50:47 event 96 1.58
818 14:50:48 event 97 1.52
819 14:50:48 event 98 1.64
820 14:50:48 event 99 1.89
821 14:53:12 event 0 1.61
822 14:53:12 event 1 1.59
823 14:53:12 event 2 1.55
824 14:53:12 event 3 1.52
825 14:53:12 event 4 1.54
826 14:53:12 event 5 1.57
827 14:53:12 event 6 1.59
828 14:53:12 event 7 1.57
829 14:53:12 event 8 1.55
830 14:53:12 event 9 1.6
831 14:53:12 event 10 1.62
832 14:53:12 event 11 1.56
833 14:53:12 event 12 1.57
834 14:53:12 event 13 1.59
835 14:53:12 event 14 1.64
836 14:53:12 event 15 1.71
837 14:53:12 event 16 1.6
838 14:53:12 event 17 1.65
839 14:53:12 event 18 1.7
840 14:53:13 event 19 1.63
841 14:53:13 event 20 1.73
842 14:53:13 event 21 1.58
843 14:53:13 event 22 1.65
844 14:53:13 event 23 1.57
845 14:53:13 event 24 1.67
846 14:53:13 event 25 1.64
847 14:53:13 event 26 1.51
848 14:53:13 event 27 2.36
849 14:53:13 event 28 1.04
850 14:53:13 event 29 1.63
851 14:53:13 event 30 1.74
852 14:53:14 event 31 1.62
853 14:53:14 event 32 1.62
854 14:53:14 event 33 1.74
855 14:53:14 event 34 1.67
856 14:53:14 event 35 1.61
857 14:53:14 event 36 1.78
858 14:53:14 event 37 1.64
859 14:53:14 event 38 1.67
860 14:53:14 event 39 1.8
861 14:53:15 event 40 1.68
862 14:53:15 event 41 1.67
863 14:53:15 event 42 1.64
864 14:53:15 event 44 1.77
865 14:53:15 event 43 1.65
866 14:53:15 event 45 1.65
867 14:53:15 event 46 1.69
868 14:53:15 event 47 1.68
869 14:53:16 event 48 1.54
870 14:53:16 event 49 1.83
871 14:53:16 event 50 1.61
872 14:53:16 event 51 1.64
873 14:53:16 event 52 1.89
874 14:53:16 event 53 1.56
875 14:53:17 event 54 1.72
876 14:53:17 event 55 1.52
877 14:53:17 event 56 1.54
878 14:53:17 event 57 1.73
879 14:53:17 event 58 1.07
880 14:53:17 event 59 1.91
881 14:53:18 event 60 1.7
882 14:53:18 event 61 3.93
883 14:53:18 event 62 1.59
884 14:53:18 event 63 1.64
885 14:53:18 event 64 1.63
886 14:53:19 event 65 1.7
887 14:53:19 event 66 1.64
888 14:53:19 event 67 1.63
889 14:53:19 event 68 1.6
890 14:53:19 event 69 1.19
891 14:53:19 event 70 3.76
892 14:53:20 event 71 1.62
893 14:53:20 event 72 1.45
894 14:53:20 event 73 1.89
895 14:53:20 event 74 1.95
896 14:53:21 event 75 1.95
897 14:53:21 event 76 1.81
898 14:53:21 event 77 1.62
899 14:53:21 event 78 1.63
900 14:53:21 event 79 2.07
901 14:53:21 event 80 1.64
902 14:53:22 event 81 1.21
903 14:53:22 event 82 1.62
904 14:53:22 event 83 1.67
905 14:53:22 event 84 1.65
906 14:53:23 event 85 1.64
907 14:53:23 event 86 1.59
908 14:53:23 event 87 1.68
909 14:53:24 event 88 1.6
910 14:53:24 event 89 4.69
911 14:53:24 event 90 1.72
912 14:53:24 event 91 1.63
913 14:53:25 event 92 1.59
914 14:53:25 event 93 1.74
915 14:53:25 event 94 1.68
916 14:53:25 event 95 1.65
917 14:53:26 event 96 1.61
918 14:53:26 event 97 1.63
919 14:53:26 event 98 1.65
920 14:53:26 event 99 1.71
921 14:56:36 event 0 1.62
922 14:56:36 event 1 1.61
923 14:56:36 event 2 1.57
924 14:56:36 event 3 1.55
925 14:56:36 event 4 1.51
926 14:56:36 event 5 1.59
927 14:56:36 event 6 1.59
928 14:56:36 event 7 1.57
929 14:56:36 event 8 1.56
930 14:56:36 event 9 1.52
931 14:56:36 event 10 1.6
932 14:56:36 event 11 1.63
933 14:56:36 event 12 1.58
934 14:56:36 event 13 1.58
935 14:56:36 event 14 1.78
936 14:56:36 event 15 1.64
937 14:56:36 event 16 1.61
938 14:56:36 event 17 1.59
939 14:56:37 event 18 1.92
940 14:56:37 event 19 1.77
941 14:56:37 event 20 1.64
942 14:56:37 event 21 1.65
943 14:56:37 event 22 1.62
944 14:56:37 event 23 1.6
945 14:56:37 event 24 1.55
946 14:56:37 event 25 1.6
947 14:56:37 event 26 1.59
948 14:56:37 event 27 1.65
949 14:56:37 event 28 1.8
950 14:56:38 event 29 1.74
951 14:56:38 event 30 1.66
952 14:56:38 event 31 1.78

View File

@@ -0,0 +1 @@
pydantic>=2.6.1

View File

@@ -0,0 +1,74 @@
import os
import random
import string
from datetime import datetime, timezone
# Optional: Using Pydantic for validation (remove if not using Pydantic)
try:
from pydantic import BaseModel
class HelloResponse(BaseModel):
message: str
status: str
appName: str
# If using Pydantic, we can generate the JSON schema
response_schema = {
200: HelloResponse.model_json_schema()
}
except ImportError:
# Without Pydantic, define JSON schema manually
response_schema = {
200: {
"type": "object",
"properties": {
"message": {"type": "string"},
"status": {"type": "string"},
"appName": {"type": "string"}
},
"required": ["message", "status", "appName"]
}
}
config = {
"name": "HelloAPI",
"type": "api",
"path": "/hello",
"method": "GET",
"description": "Receives hello request and emits event for processing",
"emits": ["process-greeting"],
"flows": ["hello-world-flow"],
"responseSchema": response_schema
}
async def handler(req, context):
app_name = os.environ.get("APP_NAME", "Motia App")
timestamp = datetime.now(timezone.utc).isoformat()
context.logger.info("Hello API endpoint called", {
"app_name": app_name,
"timestamp": timestamp
})
# Generate a random request ID
request_id = ''.join(random.choices(string.ascii_lowercase + string.digits, k=7))
# Emit event for background processing
await context.emit({
"topic": "process-greeting",
"data": {
"timestamp": timestamp,
"appName": app_name,
"greetingPrefix": os.environ.get("GREETING_PREFIX", "Hello"),
"requestId": request_id
}
})
return {
"status": 200,
"body": {
"message": "Hello request received! Check logs for processing.",
"status": "processing",
"appName": app_name
}
}

View File

@@ -0,0 +1,65 @@
import asyncio
from datetime import datetime, timezone
# Optional: Using Pydantic for validation (remove if not using Pydantic)
try:
from pydantic import BaseModel
class GreetingInput(BaseModel):
timestamp: str
appName: str
greetingPrefix: str
requestId: str
# If using Pydantic, we can generate the JSON schema
input_schema = GreetingInput.model_json_schema()
except ImportError:
# Without Pydantic, define JSON schema manually
input_schema = {
"type": "object",
"properties": {
"timestamp": {"type": "string"},
"appName": {"type": "string"},
"greetingPrefix": {"type": "string"},
"requestId": {"type": "string"}
},
"required": ["timestamp", "appName", "greetingPrefix", "requestId"]
}
config = {
"name": "ProcessGreeting",
"type": "event",
"description": "Processes greeting in the background",
"subscribes": ["process-greeting"],
"emits": [],
"flows": ["hello-world-flow"],
"input": input_schema
}
async def handler(input_data, context):
# Extract data from input
timestamp = input_data.get("timestamp")
app_name = input_data.get("appName")
greeting_prefix = input_data.get("greetingPrefix")
request_id = input_data.get("requestId")
context.logger.info("Processing greeting", {
"request_id": request_id,
"app_name": app_name
})
greeting = f"{greeting_prefix} {app_name}!"
# Store result in state (demonstrates state usage)
# Note: The state.set method takes (groupId, key, value)
await context.state.set("greetings", request_id, {
"greeting": greeting,
"processedAt": datetime.now(timezone.utc).isoformat(),
"originalTimestamp": timestamp
})
context.logger.info("Greeting processed successfully", {
"request_id": request_id,
"greeting": greeting,
"stored_in_state": True
})

View File

@@ -0,0 +1,215 @@
#!/usr/bin/env python3
"""
Performance Test Log Analyzer
Extracts timing data from journalctl logs and creates plots
"""
import subprocess
import re
import csv
from datetime import datetime, timezone
from collections import defaultdict
import matplotlib.pyplot as plt
import sys
def run_journalctl():
"""Run journalctl to get recent logs"""
try:
result = subprocess.run(
['journalctl', '--since', '1 hour ago', '--no-pager'],
capture_output=True,
text=True,
timeout=30
)
return result.stdout
except subprocess.TimeoutExpired:
print("journalctl timeout")
return ""
except FileNotFoundError:
print("journalctl not found")
return ""
def read_motia_log():
"""Read motia.log file directly"""
try:
with open('motia.log', 'r') as f:
return f.read()
except FileNotFoundError:
print("motia.log not found")
return ""
def parse_logs(logs):
"""Parse the logs to extract timing data"""
# Remove ANSI escape codes
ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
logs = ansi_escape.sub('', logs)
cron_pattern = r'\[(\d{2}:\d{2}:\d{2})\] .* Starting perf test emission at ([\d\-T:Z.]+)'
emission_pattern = r'\[(\d{2}:\d{2}:\d{2})\] .* Completed emitting 100 perf-test events in ([\d.]+)s'
event_pattern = r'\[(\d{2}:\d{2}:\d{2})\] .* Processed perf-test event (\d+) from batch .* in ([\d.]+)ms'
cron_starts = []
emissions = []
events = defaultdict(list)
for line in logs.split('\n'):
# Parse cron start
cron_match = re.search(cron_pattern, line)
if cron_match:
timestamp_str, iso_time = cron_match.groups()
cron_starts.append({
'timestamp': timestamp_str,
'iso_time': iso_time,
'batch_start': iso_time
})
# Parse emission completion
emission_match = re.search(emission_pattern, line)
if emission_match:
timestamp_str, duration = emission_match.groups()
emissions.append({
'timestamp': timestamp_str,
'duration': float(duration)
})
# Parse event processing
event_match = re.search(event_pattern, line)
if event_match:
timestamp_str, event_id, duration = event_match.groups()
events[timestamp_str].append({
'event_id': int(event_id),
'duration': float(duration)
})
# Filter out runs after the restart (keep only first 10 emission runs)
emissions = emissions[:10]
return cron_starts, emissions, events
def save_to_csv(cron_starts, emissions, events, filename='perf_test_data.csv'):
"""Save the parsed data to CSV"""
with open(filename, 'w', newline='') as csvfile:
writer = csv.writer(csvfile)
# Write header
writer.writerow(['timestamp', 'type', 'batch_start', 'emission_duration', 'event_id', 'event_duration'])
# Write cron starts
for cron in cron_starts:
writer.writerow([cron['timestamp'], 'cron_start', cron['batch_start'], '', '', ''])
# Write emissions
for emission in emissions:
writer.writerow([emission['timestamp'], 'emission', '', emission['duration'], '', ''])
# Write events
for timestamp, event_list in events.items():
for event in event_list:
writer.writerow([timestamp, 'event', '', '', event['event_id'], event['duration']])
print(f"Data saved to {filename}")
def create_plots(cron_starts, emissions, events):
"""Create plots from the data"""
if not emissions:
print("No emission data found")
return
# Plot 1: Emission durations over time
if emissions:
# Parse timestamps - handle different formats
timestamps = []
for e in emissions:
try:
# Try different formats
ts_str = e['timestamp']
if 'Okt' in ts_str: # German format
dt = datetime.strptime(ts_str, '%b %d %H:%M:%S')
else:
dt = datetime.strptime(ts_str, '%b %d %H:%M:%S')
timestamps.append(dt)
except ValueError:
# Fallback: just use index
timestamps.append(datetime.now().replace(hour=len(timestamps), minute=0, second=0, microsecond=0))
durations = [e['duration'] for e in emissions]
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
plt.plot(range(len(timestamps)), durations, 'bo-', label='Emission Duration')
plt.title('Cron Step Emission Durations')
plt.xlabel('Run Number')
plt.ylabel('Duration (seconds)')
plt.grid(True, alpha=0.3)
# Plot 2: Event processing times (box plot per batch)
if events:
plt.subplot(1, 2, 2)
batch_durations = []
batch_labels = []
for i, (timestamp, event_list) in enumerate(list(events.items())[:10]): # Show first 10 batches
if event_list:
durations = [e['duration'] for e in event_list]
batch_durations.append(durations)
batch_labels.append(f'Batch {i+1}')
if batch_durations:
plt.boxplot(batch_durations, labels=batch_labels)
plt.title('Event Processing Times per Batch')
plt.ylabel('Duration (ms)')
plt.xticks(rotation=45)
plt.tight_layout()
plt.savefig('perf_test_analysis.png', dpi=150, bbox_inches='tight')
print("Plot saved to perf_test_analysis.png")
plt.show()
def main():
print("Analyzing performance test logs...")
# Get logs from motia.log
logs = read_motia_log()
if not logs:
print("No logs retrieved")
return
# Parse the logs
cron_starts, emissions, events = parse_logs(logs)
print(f"Found {len(cron_starts)} cron starts")
print(f"Found {len(emissions)} emission completions")
print(f"Found {len(events)} event batches with {sum(len(v) for v in events.values())} total events")
if not emissions and not events:
print("No performance test data found in logs")
return
# Save to CSV
save_to_csv(cron_starts, emissions, events)
# Create plots
create_plots(cron_starts, emissions, events)
# Print summary statistics
if emissions:
emission_durations = [e['duration'] for e in emissions]
print("\nEmission Statistics:")
print(f" Average: {sum(emission_durations)/len(emission_durations):.3f}s")
print(f" Min: {min(emission_durations):.3f}s")
print(f" Max: {max(emission_durations):.3f}s")
if events:
all_event_durations = []
for event_list in events.values():
all_event_durations.extend([e['duration'] for e in event_list])
if all_event_durations:
print("\nEvent Processing Statistics:")
print(f" Average: {sum(all_event_durations)/len(all_event_durations):.3f}ms")
print(f" Min: {min(all_event_durations):.3f}ms")
print(f" Max: {max(all_event_durations):.3f}ms")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,30 @@
import asyncio
from datetime import datetime, timezone
config = {
"type": "cron",
"cron": "*/1 * * * *", # Every minute
"name": "PerfTestCron",
"description": "Emits 100 perf-test events every minute",
"emits": ["perf-test-event"],
"flows": ["perf-test"],
}
async def handler(context):
start_time = datetime.now(timezone.utc)
context.logger.info(f"Starting perf test emission at {start_time}")
# Emit 100 events
for i in range(100):
await context.emit({
"topic": "perf-test-event",
"data": {
"event_id": i,
"batch_start": start_time.isoformat(),
"timestamp": datetime.now(timezone.utc).isoformat(),
},
})
end_time = datetime.now(timezone.utc)
duration = (end_time - start_time).total_seconds()
context.logger.info(f"Completed emitting 100 perf-test events in {duration:.3f}s")

View File

@@ -0,0 +1,24 @@
import asyncio
from datetime import datetime, timezone
config = {
"type": "event",
"name": "PerfTestEventHandler",
"description": "Handles perf-test events with 1ms delay and logging",
"subscribes": ["perf-test-event"],
"emits": [],
"flows": ["perf-test"],
}
async def handler(event_data, context):
start_time = datetime.now(timezone.utc)
event_id = event_data.get("event_id")
batch_start = event_data.get("batch_start")
# Wait 1ms
await asyncio.sleep(0.001)
# Log completion with duration
end_time = datetime.now(timezone.utc)
duration = (end_time - start_time).total_seconds() * 1000 # in milliseconds
context.logger.info(f"Processed perf-test event {event_id} from batch {batch_start} in {duration:.2f}ms")

View File

@@ -0,0 +1,31 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"noEmit": true,
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"allowJs": true,
"outDir": "dist",
"rootDir": ".",
"baseUrl": ".",
"jsx": "react-jsx"
},
"include": [
"**/*.ts",
"motia.config.ts",
"**/*.tsx",
"types.d.ts",
"**/*.jsx"
],
"exclude": [
"node_modules",
"dist",
"tests"
]
}

View File

@@ -0,0 +1,21 @@
/**
* Automatically generated types for motia
* Do NOT edit this file manually.
*
* Consider adding this file to .prettierignore and eslint ignore.
*/
import { EventHandler, ApiRouteHandler, ApiResponse, MotiaStream, CronHandler } from 'motia'
declare module 'motia' {
interface FlowContextStateStreams {
}
interface Handlers {
'PerfTestEventHandler': EventHandler<never, never>
'PerfTestCron': CronHandler<{ topic: 'perf-test-event'; data: never }>
'ProcessGreeting': EventHandler<{ timestamp: string; appName: string; greetingPrefix: string; requestId: string }, never>
'HelloAPI': ApiRouteHandler<Record<string, unknown>, ApiResponse<200, { message: string; status: string; appName: string }>, { topic: 'process-greeting'; data: { timestamp: string; appName: string; greetingPrefix: string; requestId: string } }>
}
}