Initial commit with Advoware proxy

This commit is contained in:
root
2025-10-19 14:57:07 +00:00
commit 273aa8b549
45771 changed files with 5534555 additions and 0 deletions

View File

@@ -0,0 +1,96 @@
---
description: How to structure your Motia project
globs:
alwaysApply: true
---
# Architecture Guide
## Overview
This guide covers the architecture of a Motia project.
## File Structure
All step files should be underneath the `steps/` folder.
Underneath the `steps/` folder, create subfolders for Flows. Flows are used to group steps together.
## 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 to help adding common validation, error
handling and other common logic to your steps.
- Make sure to add all the middlewares in a single folder, called `middlewares/`.
- Create a comprehensive file name for the middleware, like `auth.middleware.ts`.
- Follow SOLID principles with separation of concerns in middlewares, create a middleware for each responsibility.
- Use core middleware to handle ZodError gracefully (see [Error Handling Guide](./error-handling.mdc))
- Rate limiting and CORS are not needed to be handled in middleware since they're an infrastructure concern.
## Domain Driven Design
Make sure you follow Domain Driven Design principles in your project.
- Create `/src/services` folder to store your services, this is where it holds business logic.
- Create `/src/repositories` folder to store your repositories, this is where it holds data access logic.
- Create `/src/utils` folder to store your utility functions.
- Models and DTOs are not quite necessary, we can rely on zod to create the models and DTOs from the steps.
- Controller layer is the Steps, it should have mostly logic around validation and calling services.
- Avoid having Service methods with just a call to the Repository, it should have some logic around it, if it doesn't have, then Steps can have access to repositories directly.
### Services
Defining services can be done in the following way:
- Create a folder underneath `/src/services/` folder, like `/src/services/auth/`.
- Create a file inside the folder called `index.ts`.
- Inside `index.ts`, export a constant with the name of the service, with the methods as properties.
- Methods should be defined as separate files, use export named functions.
- Use the service in the Steps.
#### Example
```typescript
/**
* Business logic for authentication defined in a separate file in the same folder.
*/
import { login } from './login'
/**
* Constant with the name of the service, with the methods as properties.
*/
export const authService = {
login
}
```
## 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,425 @@
---
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 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>[]
/**
* Defined with Zod library, can be a ZodObject OR a ZodArray.
*
* Note: This is not validated automatically, you need to validate it in the handler.
*/
bodySchema?: ZodInput
/**
* Defined with Zod library, can be a ZodObject OR a ZodArray
*
* The key (number) is the HTTP status code this endpoint can return and
* for each HTTP Status Code, you need to define a Zod schema that defines the response body
*/
responseSchema?: Record<number, ZodInput>
/**
* 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 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.any()).optional()
})
export const config: ApiRouteConfig = {
type: 'api',
name: 'CreateResource',
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 = {
"type": "api",
"name": "CreateResource",
"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 = {
type: 'cron',
name: 'CronJobEvery5Minutes', // should always be the same as Handlers['__']
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 = {
"type": "cron",
"name": "CronJobEvery5Minutes",
"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,218 @@
---
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 API 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 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[]
/**
* The Zod schema of the input data of events this step processes.
*
* 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: ZodInput
/**
* 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 `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 = {
type: 'event',
name: 'SendEmail',
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 = {
"type": "event",
"name": "SendEmail",
"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,380 @@
---
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.
### 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 = {
/**
* This is the stream name, it's extremely important to
* be used on the client side.
*/
name: 'chatMessage',
/**
* This is the schema of the data that will be stored in the stream.
*
* It helps Motia to create the types on the steps to enforce the
* streams objects are created correctly.
*/
schema: chatMessageSchema,
/**
* Let's not worry about base config for now, all streams
* have this storage type default
*/
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
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 = {
type: 'api',
name: 'CreateChatMessage',
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 = {
"type": "api",
"name": "CreateChatMessage",
"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 = {
"type": "api",
"name": "CreateChatMessage",
"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