Skip to content

Instantly share code, notes, and snippets.

@mfpiccolo
Created March 4, 2025 13:40
Show Gist options
  • Save mfpiccolo/a273133302d168925d23376e199abec2 to your computer and use it in GitHub Desktop.
Save mfpiccolo/a273133302d168925d23376e199abec2 to your computer and use it in GitHub Desktop.
{
"rules": [
// File Structure Rules
{
"name": "enforce-step-file-naming",
"description": "Ensure step files follow the *.step.{ts,js,py,rb} naming convention",
"pattern": "\\b(?!.*\\.step\\.(ts|js|py|rb)$).*step\\b",
"severity": "error",
"message": "Step files must follow the pattern: filename.step.{ts|js|py|rb}",
"example": {
"correct": "processPayment.step.ts",
"incorrect": "processPayment.ts or processPaymentStep.ts"
}
},
{
"name": "enforce-ui-step-file-naming",
"description": "Ensure UI step files match their corresponding step files",
"pattern": "\\b(?!.*\\.step\\.(tsx|jsx)$).*\\.step\\.(tsx|jsx)\\b",
"severity": "error",
"message": "UI step files must follow the pattern: filename.step.{tsx|jsx} and match a corresponding step file",
"example": {
"correct": "processPayment.step.tsx (with matching processPayment.step.ts)",
"incorrect": "processPaymentUI.tsx or processPayment.tsx"
}
},
// Step Configuration Rules
{
"name": "missing-step-config",
"description": "Check for missing config in step files",
"pattern": "\\b(?:export\\s+const\\s+config|exports\\.config|def\\s+config|config\\s+=)\\b",
"inverse": true,
"filePattern": ".*\\.step\\.(ts|js|py|rb)$",
"severity": "error",
"message": "Step files must export a config object",
"example": {
"typescript": "export const config: EventConfig = { ... }",
"javascript": "exports.config = { ... }",
"python": "config = { ... }",
"ruby": "def config\n { ... }\nend"
}
},
{
"name": "missing-step-handler",
"description": "Check for missing handler in step files",
"pattern": "\\b(?:export\\s+const\\s+handler|exports\\.handler|def\\s+handler|async\\s+def\\s+handler)\\b",
"inverse": true,
"filePattern": ".*\\.step\\.(ts|js|py|rb)$",
"severity": "error",
"message": "Step files must export a handler function",
"example": {
"typescript": "export const handler: StepHandler<typeof config> = async (input, { emit }) => { ... }",
"javascript": "exports.handler = async (input, { emit }) => { ... }",
"python": "async def handler(input, ctx):\n # ...",
"ruby": "def handler(input, ctx)\n # ...\nend"
}
},
{
"name": "missing-step-type",
"description": "Check for missing type in step config",
"pattern": "\\b(?:type:\\s*['\"](?:event|api|cron|noop)['\"])\\b",
"inverse": true,
"filePattern": ".*\\.step\\.(ts|js)$",
"severity": "error",
"message": "Step config must specify a type (event, api, cron, or noop)",
"example": {
"correct": "type: 'event', // or 'api', 'cron', 'noop'",
"incorrect": "Missing type property"
}
},
{
"name": "undefined-flow",
"description": "Check for undefined flows in step config",
"pattern": "\\bflows:\\s*\\[\\s*\\]",
"filePattern": ".*\\.step\\.(ts|js|py|rb)$",
"severity": "warning",
"message": "Step has empty flows array. Steps should belong to at least one flow for proper visualization.",
"example": {
"correct": "flows: ['payment-processing']",
"incorrect": "flows: []"
}
},
// Event Step Rules
{
"name": "event-step-subscribes",
"description": "Check that event steps have subscribes",
"pattern": "\\btype:\\s*['\"]event['\"].*?subscribes:\\s*\\[",
"inverse": true,
"filePattern": ".*\\.step\\.(ts|js)$",
"severity": "warning",
"message": "Event steps should have at least one topic they subscribe to",
"example": {
"correct": "type: 'event',\nsubscribes: ['payment.initiated']",
"incorrect": "type: 'event',\n// missing subscribes property"
}
},
{
"name": "event-step-emits",
"description": "Check that event steps have emits",
"pattern": "\\btype:\\s*['\"]event['\"].*?emits:\\s*\\[",
"inverse": true,
"filePattern": ".*\\.step\\.(ts|js)$",
"severity": "warning",
"message": "Event steps should emit at least one topic",
"example": {
"correct": "type: 'event',\nemits: ['payment.processed']",
"incorrect": "type: 'event',\n// missing emits property"
}
},
// API Step Rules
{
"name": "api-step-path",
"description": "Check that API steps have a path",
"pattern": "\\btype:\\s*['\"]api['\"].*?path:\\s*['\"]",
"inverse": true,
"filePattern": ".*\\.step\\.(ts|js)$",
"severity": "error",
"message": "API steps must have a path",
"example": {
"correct": "type: 'api',\npath: '/api/payments'",
"incorrect": "type: 'api',\n// missing path property"
}
},
{
"name": "api-step-method",
"description": "Check that API steps have a method",
"pattern": "\\btype:\\s*['\"]api['\"].*?method:\\s*['\"](?:GET|POST)['\"]",
"inverse": true,
"filePattern": ".*\\.step\\.(ts|js)$",
"severity": "error",
"message": "API steps must have a method (GET or POST)",
"example": {
"correct": "type: 'api',\nmethod: 'POST'",
"incorrect": "type: 'api',\n// missing method property"
}
},
{
"name": "api-step-path-starts-with-slash",
"description": "Check that API paths start with a slash",
"pattern": "\\bpath:\\s*['\"][^/]",
"filePattern": ".*\\.step\\.(ts|js)$",
"severity": "error",
"message": "API paths must start with a slash",
"example": {
"correct": "path: '/api/payments'",
"incorrect": "path: 'api/payments'"
}
},
// Cron Step Rules
{
"name": "cron-step-expression",
"description": "Check that cron steps have a cron expression",
"pattern": "\\btype:\\s*['\"]cron['\"].*?cron:\\s*['\"]",
"inverse": true,
"filePattern": ".*\\.step\\.(ts|js)$",
"severity": "error",
"message": "Cron steps must have a cron expression",
"example": {
"correct": "type: 'cron',\ncron: '0 * * * *'",
"incorrect": "type: 'cron',\n// missing cron property"
}
},
{
"name": "cron-step-emits",
"description": "Check that cron steps have emits",
"pattern": "\\btype:\\s*['\"]cron['\"].*?emits:\\s*\\[",
"inverse": true,
"filePattern": ".*\\.step\\.(ts|js)$",
"severity": "error",
"message": "Cron steps must emit at least one topic",
"example": {
"correct": "type: 'cron',\nemits: ['daily.cleanup']",
"incorrect": "type: 'cron',\n// missing emits property"
}
},
// NOOP Step Rules
{
"name": "noop-step-virtualemits",
"description": "Check that NOOP steps have virtualEmits",
"pattern": "\\btype:\\s*['\"]noop['\"].*?virtualEmits:\\s*\\[",
"inverse": true,
"filePattern": ".*\\.step\\.(ts|js)$",
"severity": "error",
"message": "NOOP steps must have virtualEmits",
"example": {
"correct": "type: 'noop',\nvirtualEmits: ['payment.external.processed']",
"incorrect": "type: 'noop',\n// missing virtualEmits property"
}
},
{
"name": "noop-step-virtualsubscribes",
"description": "Check that NOOP steps have virtualSubscribes (even if empty)",
"pattern": "\\btype:\\s*['\"]noop['\"].*?virtualSubscribes:\\s*\\[",
"inverse": true,
"filePattern": ".*\\.step\\.(ts|js)$",
"severity": "error",
"message": "NOOP steps must have virtualSubscribes (even if empty)",
"example": {
"correct": "type: 'noop',\nvirtualSubscribes: [] // or ['payment.external.initiated']",
"incorrect": "type: 'noop',\n// missing virtualSubscribes property"
}
},
// UI Step Rules
{
"name": "ui-step-default-export",
"description": "Check that UI step files have a default export",
"pattern": "\\bexport\\s+default\\s+",
"inverse": true,
"filePattern": ".*\\.step\\.(tsx|jsx)$",
"severity": "error",
"message": "UI step files must have a default export",
"example": {
"correct": "export default function MyStep() {\n return (\n // ...\n )\n}",
"incorrect": "function MyStep() {\n return (\n // ...\n )\n}\n// missing export default"
}
},
{
"name": "ui-step-basehandle",
"description": "Check that UI step files include BaseHandle",
"pattern": "\\bBaseHandle\\b",
"inverse": true,
"filePattern": ".*\\.step\\.(tsx|jsx)$",
"severity": "warning",
"message": "UI step files should include BaseHandle components for proper flow connections",
"example": {
"correct": "import { BaseHandle, Position } from 'motia'\n\nexport default function MyStep() {\n return (\n <div>\n <BaseHandle type=\"target\" position={Position.Top} />\n // ...\n <BaseHandle type=\"source\" position={Position.Bottom} />\n </div>\n )\n}",
"incorrect": "export default function MyStep() {\n return (\n <div>\n // Missing BaseHandle components\n </div>\n )\n}"
}
},
// Handler Function Rules
{
"name": "missing-emit-import",
"description": "Check for missing emit in handler destructuring",
"pattern": "\\bhandler.*?=.*?\\{\\s*(?!.*?emit).*?\\}",
"filePattern": ".*\\.step\\.(ts|js)$",
"severity": "warning",
"message": "Handler function should destructure the emit function if it needs to emit events",
"example": {
"correct": "export const handler = async (input, { emit, logger }) => { ... }",
"incorrect": "export const handler = async (input, { logger }) => { ... } // missing emit"
}
},
{
"name": "missing-logger-import",
"description": "Check for missing logger in handler destructuring",
"pattern": "\\bhandler.*?=.*?\\{\\s*(?!.*?logger).*?\\}",
"filePattern": ".*\\.step\\.(ts|js)$",
"severity": "warning",
"message": "Handler function should destructure the logger for proper logging",
"example": {
"correct": "export const handler = async (input, { emit, logger }) => { ... }",
"incorrect": "export const handler = async (input, { emit }) => { ... } // missing logger"
}
},
{
"name": "missing-emit-call",
"description": "Check for steps with emits that don't call emit",
"pattern": "\\bemits:\\s*\\[.*?\\].*?handler.*?{[^}]*?}",
"filePattern": ".*\\.step\\.(ts|js)$",
"severity": "warning",
"message": "Handler for step with emits should call the emit function",
"example": {
"correct": "emits: ['event.done'],\n// ...\nhandler: async (input, { emit }) => {\n await emit({\n topic: 'event.done',\n data: { result: 'success' }\n })\n}",
"incorrect": "emits: ['event.done'],\n// ...\nhandler: async (input, { emit }) => {\n // Missing emit call\n}"
}
},
// State Management Rules
{
"name": "missing-state-scope",
"description": "Check for state operations without scope",
"pattern": "\\bstate\\.(?:get|set|delete|clear)\\s*\\([^,)]*\\)",
"filePattern": ".*\\.step\\.(ts|js)$",
"severity": "error",
"message": "State operations must include a scope parameter (usually traceId)",
"example": {
"correct": "await state.get(traceId, 'user.data')",
"incorrect": "await state.get('user.data') // missing scope parameter"
}
},
{
"name": "missing-traceid-in-state",
"description": "Suggest using traceId for state operations",
"pattern": "\\bstate\\.(?:get|set|delete|clear)\\s*\\((?!traceId)[^,)]*,",
"filePattern": ".*\\.step\\.(ts|js)$",
"severity": "warning",
"message": "Consider using traceId as the scope for state operations for proper isolation",
"example": {
"correct": "await state.set(traceId, 'order.data', orderData)",
"incorrect": "await state.set('customScope', 'order.data', orderData) // consider using traceId"
}
},
// Error Handling Rules
{
"name": "missing-try-catch",
"description": "Suggest try-catch blocks for external operations",
"pattern": "\\bawait\\s+(?!emit|state|logger)\\w+",
"filePattern": ".*\\.step\\.(ts|js)$",
"severity": "info",
"message": "Consider wrapping external operations in try-catch blocks for better error handling",
"example": {
"correct": "try {\n const response = await apiClient.getData()\n // ...\n} catch (error) {\n logger.error('API call failed', { error })\n}",
"incorrect": "const response = await apiClient.getData() // consider try-catch"
}
},
{
"name": "missing-error-logging",
"description": "Check for catch blocks without error logging",
"pattern": "\\bcatch\\s*\\([^)]*\\)\\s*{[^}]*?(?!logger\\.error)[^}]*?}",
"filePattern": ".*\\.step\\.(ts|js)$",
"severity": "warning",
"message": "Catch blocks should log errors using logger.error",
"example": {
"correct": "catch (error) {\n logger.error('Operation failed', { error })\n}",
"incorrect": "catch (error) {\n // Missing error logging\n}"
}
},
// TypeScript Specific Rules
{
"name": "missing-type-annotation",
"description": "Check for missing type annotations in TypeScript",
"pattern": "\\bexport\\s+const\\s+config\\s*=",
"filePattern": ".*\\.step\\.ts$",
"severity": "warning",
"message": "Consider adding type annotations to config objects (e.g., export const config: EventConfig = ...)",
"example": {
"correct": "export const config: EventConfig = { ... }",
"incorrect": "export const config = { ... } // missing type annotation"
}
},
{
"name": "missing-handler-type",
"description": "Check for missing StepHandler type in TypeScript",
"pattern": "\\bexport\\s+const\\s+handler\\s*:\\s*StepHandler",
"inverse": true,
"filePattern": ".*\\.step\\.ts$",
"severity": "warning",
"message": "Consider using the StepHandler type for better type safety",
"example": {
"correct": "export const handler: StepHandler<typeof config> = async (input, { emit }) => { ... }",
"incorrect": "export const handler = async (input, { emit }) => { ... } // missing type"
}
},
// Topic Naming Rules
{
"name": "topic-naming-convention",
"description": "Check for topic naming convention",
"pattern": "\\b(?:subscribes|emits):\\s*\\[\\s*['\"](?!.*?\\.).*?['\"]",
"filePattern": ".*\\.step\\.(ts|js)$",
"severity": "info",
"message": "Consider using namespaced topics (e.g., 'domain.event') for better organization",
"example": {
"correct": "subscribes: ['payment.initiated', 'payment.approved']",
"incorrect": "subscribes: ['paymentInitiated', 'paymentApproved'] // consider domain.event format"
}
},
// Testing Rules
{
"name": "missing-test-file",
"description": "Check for missing test file",
"pattern": ".*\\.step\\.(ts|js)$",
"filePattern": ".*\\.step\\.(ts|js)$",
"severity": "info",
"message": "Consider creating a test file for this step",
"example": {
"test-file": "processPayment.step.test.ts (for processPayment.step.ts)"
}
}
],
"ai.enableCodeLens": true,
"ai.linters.enabled": true,
"documentation": {
"llmReference": "./llms.txt",
"description": "Reference this URL for additional context about Motia"
},
"filePatterns": {
"steps": [
"**/*.step.ts",
"**/*.step.js",
"**/*.step.py",
"**/*.step.rb",
"**/*.step.tsx"
],
"api": [
"**/*Api.step.*"
],
"cron": [
"**/*[cC]ron*.step.*"
],
"noop": [
"**/*.step.{ts,tsx,js,jsx}"
]
},
"stepStructure": {
"config": {
"type": "The step type (api, event, cron, noop)",
"name": "Unique identifier for the step",
"subscribes": "List of topics this step listens to",
"emits": "List of topics this step can emit",
"flows": "List of flow identifiers that this step belongs to",
"description": "Optional description of the step's purpose"
},
"handler": "Function that contains the business logic of the step"
},
"conventions": {
"apiSteps": "Create HTTP endpoints with path and method",
"eventSteps": "React to events and emit new events",
"cronSteps": "Schedule tasks with cron expressions",
"noopSteps": "Represent external processes or UI elements"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment