├── .gitignore ├── CONTRIBUTING.md ├── README.md └── examples ├── ai-deep-research-agent ├── .env.example ├── .gitignore ├── .mermaid │ └── research.mmd ├── README.md ├── docs │ └── deep-research1.png ├── motia-workbench.json ├── package.json ├── services │ ├── firecrawl.service.ts │ └── openai.service.ts ├── steps │ ├── analyze-content.step.ts │ ├── compile-report.step.ts │ ├── extract-content.step.ts │ ├── follow-up-research.step.ts │ ├── generate-queries.step.ts │ ├── report-api.step.ts │ ├── research-api.step.ts │ ├── search-web.step.ts │ ├── status-api.step.ts │ └── types │ │ └── research-config.ts └── tsconfig.json ├── conversation-analyzer-with-vision ├── .env.example ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .mermaid │ └── default.mmd ├── .prettierignore ├── .prettierrc ├── README.md ├── errors │ ├── BadRequestError.ts │ ├── InternalServerError.ts │ └── ServiceError.ts ├── jest.config.js ├── middlewares │ ├── withApiErrorHandler.ts │ ├── withMiddleware.ts │ └── withValidation.ts ├── motia-workbench.json ├── package.json ├── steps │ ├── 01-api-trigger.step.ts │ ├── 02-conversation-reader.step.ts │ ├── 03-vision-transcriber.step.ts │ └── 05-conversation-summarizer.step.ts ├── tests │ ├── middlewares │ │ ├── withApiErrorHandler.test.ts │ │ ├── withMiddleware.test.ts │ │ └── withValidation.test.ts │ └── steps │ │ └── 01-api-trigger.test.ts └── tsconfig.json ├── finance-agent ├── .env.example ├── .gitignore ├── .mermaid │ └── finance-workflow.mmd ├── README.md ├── docs │ └── finance-agent.png ├── motia-workbench.json ├── package.json ├── services │ ├── FinanceDataService.ts │ ├── OpenAIService.ts │ ├── ServiceFactory.ts │ ├── StateService.ts │ └── WebSearchService.ts ├── steps │ ├── finance-data.step.ts │ ├── openai-analysis.step.ts │ ├── query-api.step.ts │ ├── response-coordinator.step.ts │ ├── result-api.step.ts │ ├── save-result.step.ts │ └── web-search.step.ts └── tsconfig.json ├── github-integration-workflow ├── .env.example ├── .eslintrc.js ├── .mermaid │ ├── github-issue-management.mmd │ └── github-pr-management.mmd ├── .prettierignore ├── .prettierrc.json ├── README.md ├── __tests__ │ ├── mocks │ │ ├── github-events.mock.ts │ │ └── issue-events.mock.ts │ ├── services │ │ ├── github-client.test.ts │ │ └── openai-client.test.ts │ └── steps │ │ ├── github-webhook.test.ts │ │ ├── issue-assignee-suggester.test.ts │ │ ├── issue-assigner.test.ts │ │ ├── issue-classifier.test.ts │ │ ├── pr-classifier.test.ts │ │ ├── pr-label-assigner.test.ts │ │ ├── pr-reviewer-assigner.test.ts │ │ ├── pr-test-monitor.test.ts │ │ └── pr-webhook.test.ts ├── docs │ └── images │ │ ├── github-issue-management.png │ │ └── github-pr-management.png ├── jest.config.js ├── jest.setup.js ├── motia-workbench.json ├── package.json ├── services │ ├── github │ │ └── GithubClient.ts │ └── openai │ │ └── OpenAIClient.ts ├── steps │ ├── issue-triage │ │ ├── assignee-selector.step.ts │ │ ├── github-webhook.step.ts │ │ ├── handle-issue-closure.step.ts │ │ ├── handle-issue-update.step.ts │ │ ├── handle-new-issue.step.ts │ │ ├── issue-classifier.step.ts │ │ ├── label-assigner.step.ts │ │ ├── test-github-issue.step.ts │ │ └── test-github-issue.step.tsx │ └── pr-classifier │ │ ├── pr-classifier.step.ts │ │ ├── pr-label-assigner.step.ts │ │ ├── pr-reviewer-assigner.step.ts │ │ ├── pr-test-monitor.step.ts │ │ ├── pr-webhook.step.ts │ │ ├── test-pr-webhook.step.ts │ │ └── test-pr-webhook.step.tsx ├── tsconfig.json └── types │ ├── github-events.ts │ ├── github-pr-events.ts │ └── github.ts ├── gmail-workflow ├── .env.example ├── .gitignore ├── README.md ├── config │ ├── default.ts │ └── env.ts ├── docs │ └── images │ │ └── gmail-flow.png ├── jest.config.js ├── motia-workbench.json ├── package.json ├── requirements.txt ├── services │ ├── discord.service.ts │ ├── google-base.service.ts │ ├── google.service.ts │ └── state.service.ts ├── steps │ ├── analyze-email.step.py │ ├── api │ │ ├── gmail-auth-callback.step.ts │ │ ├── gmail-get-auth-url.step.ts │ │ ├── gmail-token-status.step.ts │ │ └── gmail-watch.step.ts │ ├── auto-responder.step.ts │ ├── daily-summary.step.ts │ ├── fetch-email.step.ts │ ├── gmail-webhook.step.ts │ ├── noops │ │ ├── gmail-webhook-simulator.step.ts │ │ ├── gmail-webhook-simulator.step.tsx │ │ ├── google-auth-noops.step.ts │ │ ├── google-auth-noops.step.tsx │ │ ├── human-review.step.ts │ │ └── human-review.step.tsx │ └── organize-email.step.ts ├── tests │ ├── auto-responder.step.test.ts │ ├── daily-summary.step.test.ts │ ├── fetch-email.step.test.ts │ ├── organize-email.step.test.ts │ └── test-utils.ts └── tsconfig.json ├── motia-parallel-execution ├── .gitignore ├── README.md ├── docs │ └── images │ │ └── motia-parallel-exec.gif ├── motia-workbench.json ├── package.json ├── steps │ ├── data-aggregator.step.ts │ ├── data-processing-api.step.ts │ ├── keyword-extraction.step.ts │ ├── processing-progress.stream.ts │ ├── sentiment-analysis.step.ts │ └── word-count.step.ts ├── tsconfig.json └── types.d.ts ├── rag-docling-weaviate-agent ├── .env.example ├── .prettierignore ├── .prettierrc ├── README.md ├── docs │ └── images │ │ └── workbench.png ├── eslint.config.mjs ├── motia-workbench.json ├── package.json ├── requirements.txt ├── steps │ ├── api-steps │ │ ├── api-process-pdfs.step.ts │ │ └── api-query-rag.step.ts │ └── event-steps │ │ ├── init-weaviate.step.ts │ │ ├── load-weaviate.step.ts │ │ ├── process-pdfs.step.py │ │ └── read-pdfs.step.ts ├── tsconfig.json └── types │ └── index.ts ├── rag_example ├── .mermaid │ └── parse-embed-rag.mmd ├── README.md ├── docs │ └── images │ │ └── parse-embed-rag.png ├── motia-workbench.json ├── package.json ├── rag_config.yml ├── requirements.txt ├── src │ ├── chunk.py │ ├── embed.py │ ├── index.py │ ├── parse.py │ └── rag.py ├── steps │ ├── chunk.step.py │ ├── embed.step.py │ ├── index.step.py │ ├── parse.step.py │ └── rag_api.step.py ├── temp │ ├── chunks │ │ └── output.json │ ├── embeddings │ │ └── embeddings_file.npy │ ├── faiss_files │ │ └── vector_index.bin │ └── text │ │ └── parsed.txt └── tsconfig.json ├── research-assistant ├── .gitignore ├── README.md ├── data │ ├── knowledge-graph.json │ ├── research-report.md │ └── workbench-image.png ├── generateReport.js ├── motia-workbench.json ├── package.json ├── requirements.txt ├── steps │ ├── analyzeBenchmarks.step.ts │ ├── analyzeCoreConcepts.step.ts │ ├── analyzeImplementationDetails.step.ts │ ├── analyzeMethodology.step.ts │ ├── analyzePaper.step.ts │ ├── analyzePaperWithGemini.step.ts │ ├── analyzeRelatedLiterature.step.ts │ ├── analyzeResearchGaps.step.ts │ ├── analyzeResults.step.ts │ ├── buildEnhancedKnowledgeGraph.step.ts │ ├── buildKnowledgeGraph.step.ts │ ├── evaluateResearchImpact.step.ts │ ├── extractConcepts.step.ts │ ├── extractConceptsWithGemini.step.ts │ ├── extractEnhancedConcepts.step.ts │ ├── extractText.step.ts │ ├── generateCodeExamples.step.ts │ ├── generateMarkdownReport.step.ts │ ├── generateSummary.step.ts │ ├── generateSummaryWithGemini.step.ts │ ├── human-review.step.ts │ ├── human-review.step.tsx │ ├── queryConcepts.step.ts │ ├── queryPaper.step.ts │ ├── recommendRelatedPapers.step.ts │ ├── research-monitor.step.ts │ ├── research-monitor.step.tsx │ ├── serveCss.step.ts │ ├── serveStatic.step.ts │ ├── uploadPaper.step.ts │ ├── webhook-simulator.step.ts │ └── webhook-simulator.step.tsx ├── test-upload.js ├── tsconfig.json ├── types │ └── knowledgeGraph.ts └── utils │ ├── cleanKnowledgeGraph.ts │ ├── generateMarkdownReport.ts │ └── knowledgeGraphUtils.ts ├── trello-flow ├── .env.example ├── Dockerfile ├── README.md ├── config │ ├── default.ts │ └── env.ts ├── docs │ └── images │ │ └── trello-manager.png ├── jest.config.js ├── jest.setup.js ├── package.json ├── services │ ├── __tests__ │ │ ├── openai.service.test.ts │ │ ├── slack.service.test.ts │ │ └── trello.service.test.ts │ ├── openai.service.ts │ ├── slack.service.ts │ └── trello.service.ts ├── steps │ ├── __tests__ │ │ ├── check-overdue-cards.test.ts │ │ ├── complete-approved-card.test.ts │ │ ├── mark-card-for-review.test.ts │ │ ├── slack-notifier.test.ts │ │ ├── start-assigned-card.test.ts │ │ ├── trello-webhook.step.test.ts │ │ └── validate-card-requirements.test.ts │ ├── check-overdue-cards.step.ts │ ├── complete-approved-card.step.ts │ ├── mark-card-for-review.step.ts │ ├── noops │ │ ├── development-completed.step.ts │ │ ├── review-completed.step.ts │ │ ├── trello-webhook-simulator.step.ts │ │ ├── trello-webhook-simulator.step.tsx │ │ └── validation-completed.step.ts │ ├── slack-notifier.step.ts │ ├── start-assigned-card.step.ts │ ├── trello-webhook-validation.step.ts │ ├── trello-webhook.step.ts │ └── validate-card-requirements.step.ts ├── test │ ├── mocks │ │ ├── trello-card.mock.ts │ │ └── trello-webhook.mock.ts │ └── test-helpers.ts ├── tsconfig.json └── types │ └── trello.ts └── vision-example ├── .env.example ├── .python-version ├── LICENSE ├── README.md ├── docs └── images │ ├── eval-agent.png │ └── generate-image.png ├── eval-reports ├── 561c6b85-c99b-45d3-8500-8975359aafa2.eval.json └── b550287e-5b01-4cb8-bec9-d052aabb4828.eval.json ├── generate-dataset.ts ├── package.json ├── requirements.txt ├── steps ├── __init__.py ├── api.step.ts ├── download_image.py ├── enhance_image_prompt.step.py ├── eval-agent │ ├── eval-agent-results.api.step.ts │ └── eval-agent.step.ts ├── evaluate_result.step.py └── generate-image.step.ts ├── tmp ├── 1146231c-50ff-4351-ade2-ce93c657f9c1.png ├── 1146231c-50ff-4351-ade2-ce93c657f9c1_report.txt ├── 2187137d-92cb-4f92-acf3-3bf669c1ac37.png ├── 2187137d-92cb-4f92-acf3-3bf669c1ac37_report.txt ├── 50bf3c53-83f2-40c7-b3fc-923c7122740e.png ├── 50bf3c53-83f2-40c7-b3fc-923c7122740e_report.txt ├── 55ca6aaa-0c0a-490b-808d-28333d9c0358.png ├── 55ca6aaa-0c0a-490b-808d-28333d9c0358_report.txt ├── 6aacba7e-2649-4a99-a0da-3afa2915d0f4.png ├── 6aacba7e-2649-4a99-a0da-3afa2915d0f4_report.txt ├── 6df32658-17f8-4d2b-8129-00677a1906b3.png ├── 6df32658-17f8-4d2b-8129-00677a1906b3_report.txt ├── 89205ac8-200a-45a9-b27e-5768bf38a12c.png ├── 89205ac8-200a-45a9-b27e-5768bf38a12c_report.txt ├── b279cdc4-ffd3-452d-9f7e-06cfc0c5a0fb.png ├── b279cdc4-ffd3-452d-9f7e-06cfc0c5a0fb_report.txt ├── dc8360ff-ad1d-4648-97db-f974606bad62.png ├── dc8360ff-ad1d-4648-97db-f974606bad62_report.txt ├── dd3e2dce-6b1e-4de9-9024-7ca60e59ef17.png ├── dd3e2dce-6b1e-4de9-9024-7ca60e59ef17_report.txt ├── f4b768a8-08a8-4ec8-8377-3d5ae3cdc31f.png └── f4b768a8-08a8-4ec8-8377-3d5ae3cdc31f_report.txt └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | .pnpm-store/ 4 | .npm 5 | package-lock.json 6 | yarn.lock 7 | pnpm-lock.yaml 8 | .yarn/cache 9 | .yarn/unplugged 10 | .yarn/build-state.yml 11 | .yarn/install-state.gz 12 | 13 | # Environment and secrets 14 | .env 15 | .env.* 16 | !.env.example 17 | 18 | # Motia generated 19 | .motia/ 20 | .mermaid/ 21 | dist/ 22 | 23 | # IDE files 24 | .cursorignore 25 | .cursor/ 26 | *.code-workspace 27 | 28 | # Python cache files 29 | */__pycache__/* 30 | *.py[cod] 31 | *.pyo 32 | 33 | # macOS system files 34 | .DS_Store 35 | .DS_Store? 36 | ._* 37 | .Spotlight-V100 38 | .Trashes -------------------------------------------------------------------------------- /examples/ai-deep-research-agent/.env.example: -------------------------------------------------------------------------------- 1 | # OpenAI API key for generating queries, analyzing content, and creating reports 2 | OPENAI_API_KEY=your-openai-api-key-here 3 | 4 | # Firecrawl API key for web search and content extraction 5 | FIRECRAWL_API_KEY=your-firecrawl-api-key-here 6 | 7 | # Optional: OpenAI model to use (defaults to gpt-4o) 8 | # OPENAI_MODEL=gpt-4-turbo 9 | 10 | # Optional: Firecrawl base URL if using a self-hosted instance 11 | # FIRECRAWL_BASE_URL=http://your-firecrawl-instance-url -------------------------------------------------------------------------------- /examples/ai-deep-research-agent/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | # Dependencies 3 | node_modules/ 4 | .pnp/ 5 | .pnp.js 6 | 7 | # Build outputs 8 | dist/ 9 | build/ 10 | out/ 11 | 12 | # TypeScript 13 | *.tsbuildinfo 14 | 15 | # Testing 16 | coverage/ 17 | 18 | # Environment variables 19 | .env 20 | .env.local 21 | .env.development.local 22 | .env.test.local 23 | .env.production.local 24 | 25 | # Logs 26 | logs/ 27 | *.log 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | pnpm-debug.log* 32 | 33 | # Editor directories and files 34 | .idea/ 35 | .vscode/* 36 | !.vscode/extensions.json 37 | !.vscode/settings.json 38 | !.vscode/tasks.json 39 | !.vscode/launch.json 40 | *.suo 41 | *.ntvs* 42 | *.njsproj 43 | *.sln 44 | *.sw? 45 | 46 | # macOS specific 47 | .DS_Store 48 | .AppleDouble 49 | .LSOverride 50 | 51 | # Motia specific 52 | .motia/cache/ 53 | 54 | # Python 55 | __pycache__/ 56 | *.py[cod] 57 | *$py.class 58 | *.so 59 | .Python 60 | env/ 61 | venv/ 62 | ENV/ 63 | .env 64 | .venv 65 | env.bak/ 66 | venv.bak/ 67 | .ipynb_checkpoints 68 | .pytest_cache/ 69 | .pnpm-lock.yaml 70 | 71 | # Misc 72 | .cache/ 73 | .temp/ 74 | 75 | .cursorrules 76 | .cursor/ 77 | .motia/ -------------------------------------------------------------------------------- /examples/ai-deep-research-agent/.mermaid/research.mmd: -------------------------------------------------------------------------------- 1 | flowchart TD 2 | classDef apiStyle fill:#f96,stroke:#333,stroke-width:2px,color:#fff 3 | classDef eventStyle fill:#69f,stroke:#333,stroke-width:2px,color:#fff 4 | classDef cronStyle fill:#9c6,stroke:#333,stroke-width:2px,color:#fff 5 | classDef noopStyle fill:#3f3a50,stroke:#333,stroke-width:2px,color:#fff 6 | steps_analyze_content_step["⚡ Analyze Content"]:::eventStyle 7 | steps_compile_report_step["⚡ Compile Research Report"]:::eventStyle 8 | steps_extract_content_step["⚡ Extract Web Content"]:::eventStyle 9 | steps_follow_up_research_step["⚡ Follow-up Research"]:::eventStyle 10 | steps_generate_queries_step["⚡ Generate Search Queries"]:::eventStyle 11 | steps_report_api_step["🌐 Research Report API"]:::apiStyle 12 | steps_research_api_step["🌐 Deep Research API"]:::apiStyle 13 | steps_search_web_step["⚡ Web Search"]:::eventStyle 14 | steps_status_api_step["🌐 Research Status API"]:::apiStyle 15 | steps_analyze_content_step -->|Analysis completed| steps_compile_report_step 16 | steps_analyze_content_step -->|Follow-up research needed| steps_follow_up_research_step 17 | steps_extract_content_step -->|Content extracted| steps_analyze_content_step 18 | steps_follow_up_research_step -->|Search queries generated| steps_search_web_step 19 | steps_generate_queries_step -->|Search queries generated| steps_search_web_step 20 | steps_research_api_step -->|Research process started| steps_generate_queries_step 21 | steps_search_web_step -->|Search results collected| steps_extract_content_step 22 | -------------------------------------------------------------------------------- /examples/ai-deep-research-agent/docs/deep-research1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MotiaDev/motia-examples/c1ea1854d55a9d75218e6c00a8302dcb98289bb8/examples/ai-deep-research-agent/docs/deep-research1.png -------------------------------------------------------------------------------- /examples/ai-deep-research-agent/motia-workbench.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": { 3 | "steps/api.step.ts": { 4 | "x": 0, 5 | "y": 0 6 | }, 7 | "steps/one.step.ts": { 8 | "x": 18, 9 | "y": 236 10 | }, 11 | "steps/two.step.ts": { 12 | "x": 8, 13 | "y": 412 14 | } 15 | }, 16 | "research": { 17 | "steps/analyze-content.step.ts": { 18 | "x": 176.64812704156816, 19 | "y": 336.3731156239254 20 | }, 21 | "steps/compile-report.step.ts": { 22 | "x": -128.22588060939157, 23 | "y": 521.7708601599311 24 | }, 25 | "steps/extract-content.step.ts": { 26 | "x": 161.67393536324775, 27 | "y": 72.7514542247867 28 | }, 29 | "steps/follow-up-research.step.ts": { 30 | "x": 166.13397301722205, 31 | "y": 666.9093534377828 32 | }, 33 | "steps/generate-queries.step.ts": { 34 | "x": 666.8959354531422, 35 | "y": 117.82206970610375 36 | }, 37 | "steps/report-api.step.ts": { 38 | "x": 277.99357414977885, 39 | "y": -183.43116254806148 40 | }, 41 | "steps/research-api.step.ts": { 42 | "x": 617.2619376336042, 43 | "y": -183.68970344677325 44 | }, 45 | "steps/search-web.step.ts": { 46 | "x": 710.3514366789453, 47 | "y": 450.7226980130436 48 | }, 49 | "steps/status-api.step.ts": { 50 | "x": -123.15845212892978, 51 | "y": -185.3889865068358 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /examples/ai-deep-research-agent/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-motia-project", 3 | "description": "", 4 | "scripts": { 5 | "dev": "motia dev --verbose", 6 | "dev:debug": "motia dev --debug", 7 | "generate:config": "motia get-config --output ./" 8 | }, 9 | "keywords": [ 10 | "motia" 11 | ], 12 | "dependencies": { 13 | "@mendable/firecrawl-js": "^1.21.0", 14 | "@motiadev/core": "0.1.0-beta.15", 15 | "@motiadev/workbench": "0.1.0-beta.15", 16 | "firecrawl": "^1.21.0", 17 | "motia": "0.1.0-beta.15", 18 | "node-fetch": "^3.3.2", 19 | "openai": "^4.89.0", 20 | "react": "^19.0.0", 21 | "zod": "^3.24.2" 22 | }, 23 | "devDependencies": { 24 | "@types/node-fetch": "^2.6.12", 25 | "@types/react": "^19.0.12", 26 | "ts-node": "^10.9.2", 27 | "typescript": "^5.8.2" 28 | } 29 | } -------------------------------------------------------------------------------- /examples/ai-deep-research-agent/steps/follow-up-research.step.ts: -------------------------------------------------------------------------------- 1 | import { EventConfig, StepHandler } from '@motiadev/core' 2 | import { z } from 'zod' 3 | 4 | type Input = typeof inputSchema 5 | 6 | const inputSchema = z.object({ 7 | followUpQueries: z.array(z.string()), 8 | requestId: z.string(), 9 | originalQuery: z.string(), 10 | depth: z.number().int(), 11 | previousAnalysis: z.object({ 12 | summary: z.string(), 13 | keyFindings: z.array(z.string()), 14 | sources: z.array(z.object({ 15 | title: z.string(), 16 | url: z.string() 17 | })) 18 | }) 19 | }) 20 | 21 | export const config: EventConfig = { 22 | type: 'event', 23 | name: 'Follow-up Research', 24 | description: 'Process follow-up research queries for deeper investigation', 25 | subscribes: ['follow-up-research-needed'], 26 | emits: [{ 27 | topic: 'search-queries-generated', 28 | label: 'Search queries generated', 29 | }], 30 | input: inputSchema, 31 | flows: ['research'], 32 | } 33 | 34 | export const handler: StepHandler = async (input, { traceId, logger, state, emit }) => { 35 | logger.info('Processing follow-up research queries', { 36 | queriesCount: input.followUpQueries.length, 37 | depth: input.depth 38 | }) 39 | 40 | try { 41 | // Store the follow-up queries in state 42 | await state.set(traceId, `followUpQueries-depth-${input.depth}`, input.followUpQueries) 43 | 44 | // Pass the follow-up queries directly to the search step 45 | await emit({ 46 | topic: 'search-queries-generated', 47 | data: { 48 | searchQueries: input.followUpQueries, 49 | requestId: input.requestId, 50 | originalQuery: input.originalQuery, 51 | depth: input.depth 52 | } 53 | }) 54 | 55 | } catch (error) { 56 | logger.error('Error processing follow-up research', { error }) 57 | throw error 58 | } 59 | } -------------------------------------------------------------------------------- /examples/ai-deep-research-agent/steps/generate-queries.step.ts: -------------------------------------------------------------------------------- 1 | import { EventConfig, StepHandler } from '@motiadev/core' 2 | import { z } from 'zod' 3 | import { OpenAIService } from '../services/openai.service' 4 | import { ResearchConfig } from './types/research-config' 5 | 6 | type Input = typeof inputSchema 7 | 8 | const inputSchema = z.object({ 9 | query: z.string(), 10 | breadth: z.number().int(), 11 | depth: z.number().int(), 12 | requestId: z.string() 13 | }) 14 | 15 | export const config: EventConfig = { 16 | type: 'event', 17 | name: 'Generate Search Queries', 18 | description: 'Generate search queries based on the research topic', 19 | subscribes: ['research-started'], 20 | emits: [{ 21 | topic: 'search-queries-generated', 22 | label: 'Search queries generated', 23 | }], 24 | input: inputSchema, 25 | flows: ['research'], 26 | } 27 | 28 | export const handler: StepHandler = async (input, { traceId, logger, state, emit }) => { 29 | logger.info('Generating search queries for research topic', input) 30 | 31 | try { 32 | // Use the OpenAI service to generate search queries 33 | const openAIService = new OpenAIService() 34 | const searchQueries = await openAIService.generateSearchQueries(input.query, input.breadth) 35 | 36 | logger.info('Generated search queries', { searchQueries }) 37 | 38 | // Store the search queries in state 39 | await state.set(traceId, 'searchQueries', searchQueries) 40 | await state.set(traceId, 'originalQuery', input.query) 41 | await state.set(traceId, 'researchConfig', { 42 | breadth: input.breadth, 43 | depth: input.depth, 44 | currentDepth: 0 45 | }) 46 | 47 | // Emit event with the generated queries 48 | await emit({ 49 | topic: 'search-queries-generated', 50 | data: { 51 | searchQueries, 52 | requestId: input.requestId, 53 | originalQuery: input.query, 54 | depth: 0, 55 | } 56 | }) 57 | } catch (error) { 58 | logger.error('Error generating search queries', { error }) 59 | throw error 60 | } 61 | } -------------------------------------------------------------------------------- /examples/ai-deep-research-agent/steps/report-api.step.ts: -------------------------------------------------------------------------------- 1 | import { ApiRouteConfig, StepHandler } from '@motiadev/core' 2 | import { z } from 'zod' 3 | 4 | const inputSchema = z.object({ 5 | requestId: z.string() 6 | }) 7 | 8 | export const config: ApiRouteConfig = { 9 | type: 'api', 10 | name: 'Research Report API', 11 | description: 'API endpoint to retrieve research reports', 12 | path: '/research/report', 13 | method: 'GET', 14 | emits: [], 15 | bodySchema: inputSchema, 16 | flows: ['research'], 17 | } 18 | 19 | export const handler: StepHandler = async (req, { logger, state }) => { 20 | const requestId = req.queryParams.requestId as string 21 | logger.info('Retrieving research report', { requestId }) 22 | 23 | try { 24 | // Retrieve the final report from state 25 | const finalReport = await state.get(requestId, 'finalReport') 26 | 27 | if (!finalReport) { 28 | return { 29 | status: 404, 30 | body: { 31 | message: 'Research report not found', 32 | requestId 33 | }, 34 | } 35 | } 36 | 37 | return { 38 | status: 200, 39 | body: { 40 | message: 'Research report retrieved successfully', 41 | report: finalReport, 42 | requestId 43 | }, 44 | } 45 | } catch (error: any) { 46 | logger.error('Error retrieving research report', { requestId, error }) 47 | 48 | return { 49 | status: 500, 50 | body: { 51 | message: 'Failed to retrieve research report', 52 | requestId, 53 | error: error.message 54 | }, 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /examples/ai-deep-research-agent/steps/research-api.step.ts: -------------------------------------------------------------------------------- 1 | import { ApiRouteConfig, StepHandler } from '@motiadev/core' 2 | import { z } from 'zod' 3 | 4 | const inputSchema = z.object({ 5 | query: z.string().min(1, "Research query is required"), 6 | breadth: z.number().int().min(1).max(10).default(4), 7 | depth: z.number().int().min(1).max(5).default(2), 8 | }) 9 | 10 | export const config: ApiRouteConfig = { 11 | type: 'api', 12 | name: 'Deep Research API', 13 | description: 'API endpoint to start a deep research process', 14 | path: '/research', 15 | method: 'POST', 16 | emits: [{ 17 | topic: 'research-started', 18 | label: 'Research process started', 19 | }], 20 | bodySchema: inputSchema, 21 | flows: ['research'], 22 | } 23 | 24 | export const handler: StepHandler = async (req, { logger, emit, traceId }) => { 25 | logger.info('Starting deep research process', { 26 | query: req.body.query, 27 | breadth: req.body.breadth, 28 | depth: req.body.depth, 29 | traceId 30 | }) 31 | 32 | await emit({ 33 | topic: 'research-started', 34 | data: { 35 | query: req.body.query, 36 | breadth: req.body.breadth, 37 | depth: req.body.depth, 38 | requestId: traceId 39 | }, 40 | }) 41 | 42 | return { 43 | status: 200, 44 | body: { 45 | message: 'Research process started', 46 | requestId: traceId 47 | }, 48 | } 49 | } -------------------------------------------------------------------------------- /examples/ai-deep-research-agent/steps/search-web.step.ts: -------------------------------------------------------------------------------- 1 | import { EventConfig, StepHandler } from '@motiadev/core' 2 | import { z } from 'zod' 3 | import { FirecrawlService } from '../services/firecrawl.service' 4 | 5 | type Input = typeof inputSchema 6 | 7 | const inputSchema = z.object({ 8 | searchQueries: z.array(z.string()), 9 | requestId: z.string(), 10 | originalQuery: z.string(), 11 | depth: z.number().int() 12 | }) 13 | 14 | export const config: EventConfig = { 15 | type: 'event', 16 | name: 'Web Search', 17 | description: 'Perform web searches using Firecrawl', 18 | subscribes: ['search-queries-generated'], 19 | emits: [{ 20 | topic: 'search-results-collected', 21 | label: 'Search results collected', 22 | }], 23 | input: inputSchema, 24 | flows: ['research'], 25 | } 26 | 27 | export const handler: StepHandler = async (input, { traceId, logger, state, emit }) => { 28 | logger.info('Performing web searches', { 29 | numberOfQueries: input.searchQueries.length, 30 | depth: input.depth 31 | }) 32 | 33 | const firecrawlService = new FirecrawlService() 34 | const searchResults = [] 35 | 36 | // Process each search query sequentially to avoid rate limiting 37 | for (const query of input.searchQueries) { 38 | logger.info('Executing search query', { query }) 39 | 40 | try { 41 | // Use the FirecrawlService to perform the search 42 | const results = await firecrawlService.search({ query }, logger) 43 | 44 | // Add the search results to our collection 45 | searchResults.push({ 46 | query, 47 | results 48 | }) 49 | } catch (error) { 50 | logger.error('Error during web search', { query, error }) 51 | // Continue with other queries even if one fails 52 | } 53 | } 54 | 55 | // Store the search results in state 56 | await state.set(traceId, `searchResults-depth-${input.depth}`, searchResults) 57 | 58 | // Emit event with the collected search results 59 | await emit({ 60 | topic: 'search-results-collected', 61 | data: { 62 | searchResults, 63 | requestId: input.requestId, 64 | originalQuery: input.originalQuery, 65 | depth: input.depth 66 | } 67 | }) 68 | } -------------------------------------------------------------------------------- /examples/ai-deep-research-agent/steps/status-api.step.ts: -------------------------------------------------------------------------------- 1 | import { ApiRouteConfig, StepHandler } from '@motiadev/core' 2 | import { z } from 'zod' 3 | import { ResearchConfig } from './types/research-config' 4 | 5 | const inputSchema = z.object({ 6 | requestId: z.string() 7 | }) 8 | 9 | export const config: ApiRouteConfig = { 10 | type: 'api', 11 | name: 'Research Status API', 12 | description: 'API endpoint to check the status of a research process', 13 | path: '/research/status', 14 | method: 'GET', 15 | emits: [], 16 | bodySchema: inputSchema, 17 | flows: ['research'], 18 | } 19 | 20 | export const handler: StepHandler = async (req, { logger, state }) => { 21 | const requestId = req.queryParams.requestId as string 22 | logger.info('Checking research status', { requestId }) 23 | 24 | try { 25 | // Retrieve original query and research config 26 | const originalQuery = await state.get(requestId, 'originalQuery') 27 | const researchConfig = await state.get(requestId, 'researchConfig') 28 | const finalReport = await state.get(requestId, 'finalReport') 29 | 30 | if (!originalQuery || !researchConfig) { 31 | return { 32 | status: 404, 33 | body: { 34 | message: 'Research not found', 35 | requestId 36 | }, 37 | } 38 | } 39 | 40 | const status = finalReport ? 'completed' : 'in-progress' 41 | const progress = researchConfig ? { 42 | currentDepth: researchConfig.currentDepth, 43 | totalDepth: researchConfig.depth, 44 | percentComplete: Math.round((researchConfig.currentDepth / researchConfig.depth) * 100) 45 | } : null 46 | 47 | return { 48 | status: 200, 49 | body: { 50 | message: 'Research status retrieved successfully', 51 | requestId, 52 | originalQuery, 53 | status, 54 | progress, 55 | reportAvailable: !!finalReport 56 | }, 57 | } 58 | } catch (error: any) { 59 | logger.error('Error checking research status', { requestId, error }) 60 | 61 | return { 62 | status: 500, 63 | body: { 64 | message: 'Failed to check research status', 65 | requestId, 66 | error: error.message 67 | }, 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /examples/ai-deep-research-agent/steps/types/research-config.ts: -------------------------------------------------------------------------------- 1 | export interface ResearchConfig { 2 | breadth: number; 3 | depth: number; 4 | currentDepth: number; 5 | } 6 | -------------------------------------------------------------------------------- /examples/ai-deep-research-agent/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "resolveJsonModule": true, 11 | "allowJs": true, 12 | "outDir": "dist", 13 | "rootDir": ".", 14 | "baseUrl": ".", 15 | "jsx": "react-jsx" 16 | }, 17 | "include": [ 18 | "**/*.ts", 19 | "**/*.tsx", 20 | "**/*.js", 21 | "**/*.jsx" 22 | ], 23 | "exclude": [ 24 | "node_modules", 25 | "dist", 26 | "tests" 27 | ] 28 | } -------------------------------------------------------------------------------- /examples/conversation-analyzer-with-vision/.env.example: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY= -------------------------------------------------------------------------------- /examples/conversation-analyzer-with-vision/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .motia 3 | dist 4 | build 5 | coverage -------------------------------------------------------------------------------- /examples/conversation-analyzer-with-vision/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:prettier/recommended" 10 | ], 11 | "parser": "@typescript-eslint/parser", 12 | "parserOptions": { 13 | "ecmaVersion": "latest", 14 | "sourceType": "module" 15 | }, 16 | "plugins": ["@typescript-eslint", "prettier"], 17 | "rules": { 18 | "prettier/prettier": "error", 19 | "no-unused-vars": "off", 20 | "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], 21 | "@typescript-eslint/explicit-function-return-type": "off", 22 | "@typescript-eslint/no-explicit-any": "warn" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/conversation-analyzer-with-vision/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | .motia 4 | coverage 5 | conversations -------------------------------------------------------------------------------- /examples/conversation-analyzer-with-vision/.mermaid/default.mmd: -------------------------------------------------------------------------------- 1 | flowchart TD 2 | classDef apiStyle fill:#f96,stroke:#333,stroke-width:2px,color:#fff 3 | classDef eventStyle fill:#69f,stroke:#333,stroke-width:2px,color:#fff 4 | classDef cronStyle fill:#9c6,stroke:#333,stroke-width:2px,color:#fff 5 | classDef noopStyle fill:#3f3a50,stroke:#333,stroke-width:2px,color:#fff 6 | steps_01_api_trigger_step["🌐 Conversation Reader Trigger"]:::apiStyle 7 | steps_02_conversation_reader_step["⚡ Conversation Screenshot Reader"]:::eventStyle 8 | steps_03_vision_transcriber_step["⚡ Conversation Screenshot Transcriber"]:::eventStyle 9 | steps_04_transcription_file_writer_step["⚡ Transcription File Writer"]:::eventStyle 10 | steps_05_conversation_summarizer_step["⚡ Conversation Summarizer"]:::eventStyle 11 | steps_01_api_trigger_step -->|conversation-reader-start| steps_02_conversation_reader_step 12 | steps_02_conversation_reader_step -->|conversation-reader-complete| steps_03_vision_transcriber_step 13 | steps_03_vision_transcriber_step -->|conversation-transcription-complete| steps_04_transcription_file_writer_step 14 | steps_04_transcription_file_writer_step -->|transcription-files-written| steps_05_conversation_summarizer_step 15 | -------------------------------------------------------------------------------- /examples/conversation-analyzer-with-vision/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .motia 3 | dist 4 | build 5 | coverage 6 | pnpm-lock.yaml 7 | .DS_Store -------------------------------------------------------------------------------- /examples/conversation-analyzer-with-vision/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "es5", 4 | "singleQuote": true, 5 | "printWidth": 100, 6 | "tabWidth": 2, 7 | "endOfLine": "auto" 8 | } -------------------------------------------------------------------------------- /examples/conversation-analyzer-with-vision/errors/BadRequestError.ts: -------------------------------------------------------------------------------- 1 | import { ServiceError } from './ServiceError'; 2 | 3 | /** 4 | * Error for bad requests (400) 5 | */ 6 | export class BadRequestError extends ServiceError { 7 | constructor(message: string, details?: Record) { 8 | super(message, 400, details); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/conversation-analyzer-with-vision/errors/InternalServerError.ts: -------------------------------------------------------------------------------- 1 | import { ServiceError } from './ServiceError'; 2 | 3 | /** 4 | * Error for internal server errors (500) 5 | */ 6 | export class InternalServerError extends ServiceError { 7 | constructor(message: string, details?: Record) { 8 | super(message, 500, details); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/conversation-analyzer-with-vision/errors/ServiceError.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Base error class for step-related errors 3 | * Allows specifying status code and additional details 4 | */ 5 | export class ServiceError extends Error { 6 | status: number; 7 | details?: Record; 8 | 9 | constructor(message: string, status: number = 500, details?: Record) { 10 | super(message); 11 | this.name = this.constructor.name; 12 | this.status = status; 13 | this.details = details; 14 | 15 | // Maintains proper stack trace for where our error was thrown (only available on V8) 16 | if (Error.captureStackTrace) { 17 | Error.captureStackTrace(this, this.constructor); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/conversation-analyzer-with-vision/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | roots: [''], 5 | testMatch: ['**/tests/**/*.ts?(x)', '**/?(*.)+(spec|test).ts?(x)'], 6 | transform: { 7 | '^.+\\.tsx?$': ['ts-jest', { 8 | tsconfig: 'tsconfig.json' 9 | }] 10 | }, 11 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 12 | collectCoverageFrom: [ 13 | 'steps/**/*.ts', 14 | 'middlewares/**/*.ts', 15 | 'errors/**/*.ts', 16 | '!**/node_modules/**', 17 | '!**/dist/**', 18 | '!**/tests/**', 19 | ], 20 | coverageThreshold: { 21 | global: { 22 | branches: 80, 23 | functions: 80, 24 | lines: 80, 25 | statements: 80, 26 | }, 27 | }, 28 | coverageReporters: ['json', 'lcov', 'text', 'clover', 'html'], 29 | verbose: true, 30 | testTimeout: 10000, 31 | //setupFilesAfterEnv: ['/tests/jest.setup.ts'], 32 | }; -------------------------------------------------------------------------------- /examples/conversation-analyzer-with-vision/middlewares/withApiErrorHandler.ts: -------------------------------------------------------------------------------- 1 | import { ServiceError } from '../errors/ServiceError'; 2 | import { 3 | ApiRequest, 4 | ApiResponse, 5 | ApiRouteHandler, 6 | CronHandler, 7 | EventHandler, 8 | FlowContext, 9 | } from '@motiadev/core'; 10 | import { ZodObject } from 'zod'; 11 | 12 | /** 13 | * Middleware to handle errors in API handlers 14 | * @param handler The API handler function to wrap 15 | * @returns A wrapped API handler function with error handling 16 | */ 17 | export function withApiErrorHandler(handler: ApiRouteHandler): ApiRouteHandler { 18 | return async (req: ApiRequest, context: FlowContext): Promise => { 19 | try { 20 | return await handler(req, context); 21 | } catch (error) { 22 | context.logger.error('Error in API handler', error); 23 | 24 | if (error instanceof ServiceError) { 25 | return { 26 | status: error.status, 27 | body: { 28 | error: error.message, 29 | ...(error.details && { details: error.details }), 30 | }, 31 | }; 32 | } 33 | 34 | return { 35 | status: 500, 36 | body: { 37 | error: 'Internal server error', 38 | message: error instanceof Error ? error.message : 'Unknown error', 39 | }, 40 | }; 41 | } 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /examples/conversation-analyzer-with-vision/middlewares/withMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { StepHandler, StepConfig } from 'motia'; 2 | 3 | type Middleware, TConfig extends StepConfig = StepConfig> = ( 4 | handler: T 5 | ) => T; 6 | 7 | export const withMiddleware = < 8 | T extends StepHandler, 9 | TConfig extends StepConfig = StepConfig, 10 | >( 11 | ...fns: [...Middleware[], T] 12 | ): T => { 13 | const handler = fns.pop() as T; 14 | return (fns as Middleware[]).reduceRight((h, m) => m(h), handler); 15 | }; 16 | -------------------------------------------------------------------------------- /examples/conversation-analyzer-with-vision/middlewares/withValidation.ts: -------------------------------------------------------------------------------- 1 | import { ZodSchema, ZodError } from 'zod'; 2 | import { ApiRouteHandler, ApiRequest, FlowContext, ApiResponse } from 'motia'; 3 | import { BadRequestError } from '../errors/BadRequestError'; 4 | 5 | /** 6 | * Middleware to validate API request body against a Zod schema 7 | * @param schema The Zod schema to validate against 8 | * @returns A middleware function that validates the request body before passing to the handler 9 | */ 10 | export function withValidation>(schema: ZodSchema) { 11 | return (handler: ApiRouteHandler): ApiRouteHandler => { 12 | return async (req: ApiRequest, context: FlowContext): Promise => { 13 | try { 14 | const validatedBody = schema.parse(req.body); 15 | const validatedReq: ApiRequest = { 16 | ...req, 17 | body: validatedBody, 18 | }; 19 | 20 | return await handler(validatedReq, context); 21 | } catch (error) { 22 | if (error instanceof ZodError) { 23 | throw new BadRequestError('Validation failed', { 24 | errors: error.errors, 25 | }); 26 | } 27 | 28 | throw error; 29 | } 30 | }; 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /examples/conversation-analyzer-with-vision/motia-workbench.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": { 3 | "steps/api.step.ts": { 4 | "x": 0, 5 | "y": 0 6 | }, 7 | "steps/one.step.ts": { 8 | "x": 18, 9 | "y": 236 10 | }, 11 | "steps/two.step.ts": { 12 | "x": 8, 13 | "y": 412 14 | }, 15 | "steps/01-conversation-reader.step.ts": { 16 | "x": -50.91263383297651, 17 | "y": 0 18 | }, 19 | "steps/02-vision-transcriber.step.ts": { 20 | "x": 25.899785867237625, 21 | "y": 235.63982869379018 22 | }, 23 | "steps/03-vision-transcriber.step.ts": { 24 | "x": 380.77870563674327, 25 | "y": 384.4952762334432 26 | }, 27 | "steps/02-conversation-reader.step.ts": { 28 | "x": 394.8830897703549, 29 | "y": 247.91649269311063 30 | }, 31 | "steps/01-api-trigger.step.ts": { 32 | "x": 366, 33 | "y": 0 34 | }, 35 | "steps/04-transcription-file-writer.step.ts": { 36 | "x": 423.4793814855573, 37 | "y": 516.3158223109 38 | }, 39 | "steps/05-conversation-summarizer.step.ts": { 40 | "x": 416.66961892101006, 41 | "y": 660.4834827053778 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /examples/conversation-analyzer-with-vision/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "motia-conversation-analyzer", 3 | "description": "Motia workflow for conversation transcription, summarizing, and sentiment analysis", 4 | "scripts": { 5 | "dev": "motia dev", 6 | "dev:debug": "motia dev --debug", 7 | "generate:config": "motia get-config --output ./", 8 | "lint": "eslint . --ext .ts,.tsx", 9 | "lint:fix": "eslint . --ext .ts,.tsx --fix", 10 | "format": "prettier --write \"**/*.{ts,tsx,json,md}\"", 11 | "format:check": "prettier --check \"**/*.{ts,tsx,json,md}\"", 12 | "test": "jest --coverage" 13 | }, 14 | "keywords": [ 15 | "motia" 16 | ], 17 | "dependencies": { 18 | "@motiadev/core": "0.1.0-beta.15", 19 | "@motiadev/workbench": "0.1.0-beta.15", 20 | "motia": "0.1.0-beta.15", 21 | "openai": "^4.90.0", 22 | "react": "^19.0.0", 23 | "zod": "^3.24.2" 24 | }, 25 | "devDependencies": { 26 | "@jest/globals": "^29.7.0", 27 | "@types/jest": "^29.5.14", 28 | "@types/node": "^22.13.14", 29 | "@types/react": "^19.0.12", 30 | "@typescript-eslint/eslint-plugin": "^8.28.0", 31 | "@typescript-eslint/parser": "^8.28.0", 32 | "dotenv": "^16.4.7", 33 | "eslint": "^9.23.0", 34 | "eslint-config-prettier": "^10.1.1", 35 | "eslint-plugin-prettier": "^5.2.5", 36 | "jest": "^29.7.0", 37 | "jest-environment-node": "^29.7.0", 38 | "prettier": "^3.5.3", 39 | "ts-jest": "^29.3.0", 40 | "ts-node": "^10.9.2", 41 | "typescript": "^5.8.2" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/conversation-analyzer-with-vision/steps/01-api-trigger.step.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { z } from 'zod'; 4 | import { ApiRequest, ApiRouteConfig, FlowContext } from 'motia'; 5 | 6 | import { withValidation } from '../middlewares/withValidation'; 7 | import { withApiErrorHandler } from '../middlewares/withApiErrorHandler'; 8 | import { withMiddleware } from '../middlewares/withMiddleware'; 9 | import { BadRequestError } from '../errors/BadRequestError'; 10 | 11 | const TriggerWorkflowInputSchema = z.object({ 12 | folderPath: z.string().optional().default('conversations/real-estate-negotiation'), 13 | }); 14 | 15 | export const config: ApiRouteConfig = { 16 | type: 'api', 17 | name: 'Conversation Reader Trigger', 18 | description: 'Triggers the conversation screenshot reading process', 19 | path: '/conversation-analyzer/start', 20 | method: 'POST', 21 | emits: ['conversation-reader-start'], 22 | // bodySchema: TriggerWorkflowInputSchema, // Explicitly disabling here to prove a concept with api middlewares 23 | flows: ['default'], 24 | }; 25 | 26 | export const handler = withMiddleware( 27 | withApiErrorHandler, 28 | withValidation(TriggerWorkflowInputSchema), 29 | 30 | async (req: ApiRequest, context: FlowContext) => { 31 | const { folderPath } = req.body; 32 | context.logger.info(`Triggering conversation screenshot reader from ${folderPath}`, req); 33 | 34 | // Check if folder exists 35 | const fullPath = path.resolve(process.cwd(), folderPath); 36 | if (!fs.existsSync(fullPath)) { 37 | throw new BadRequestError(`Folder ${folderPath} does not exist`); 38 | } 39 | 40 | // Emit event to start the background processing 41 | await context.emit({ 42 | topic: 'conversation-reader-start', 43 | data: { 44 | folderPath, 45 | traceId: context.traceId, 46 | }, 47 | }); 48 | 49 | return { 50 | status: 200, 51 | body: { 52 | message: `Successfully triggered the conversation screenshot processing for folder: ${folderPath}`, 53 | folderPath, 54 | traceId: context.traceId, 55 | }, 56 | }; 57 | } 58 | ); 59 | -------------------------------------------------------------------------------- /examples/conversation-analyzer-with-vision/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "resolveJsonModule": true, 11 | "allowJs": true, 12 | "outDir": "dist", 13 | "rootDir": ".", 14 | "baseUrl": ".", 15 | "jsx": "react-jsx", 16 | "types": ["node", "jest"], 17 | "isolatedModules": true, 18 | "noImplicitAny": false 19 | }, 20 | "include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"], 21 | "exclude": ["node_modules", "dist", "tests"] 22 | } 23 | -------------------------------------------------------------------------------- /examples/finance-agent/.env.example: -------------------------------------------------------------------------------- 1 | # OpenAI API Key (required for AI analysis) 2 | OPENAI_API_KEY=your_openai_api_key_here 3 | 4 | # Alpha Vantage API Key (required for financial data) 5 | ALPHA_VANTAGE_API_KEY=your_alpha_vantage_api_key_here 6 | 7 | # SerperDev API Key (required for web search) 8 | SERPER_API_KEY=your_serper_api_key_here 9 | 10 | # Optional: Configure state storage 11 | # STATE_ADAPTER=redis 12 | # STATE_HOST=localhost 13 | # STATE_PORT=6379 14 | # STATE_TTL=3600 -------------------------------------------------------------------------------- /examples/finance-agent/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | .pnp/ 4 | .pnp.js 5 | 6 | # Build outputs 7 | dist/ 8 | build/ 9 | out/ 10 | 11 | # TypeScript 12 | *.tsbuildinfo 13 | 14 | # Testing 15 | coverage/ 16 | 17 | # Environment variables 18 | .env 19 | .env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | 24 | # Logs 25 | logs/ 26 | *.log 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | pnpm-debug.log* 31 | 32 | # Editor directories and files 33 | .idea/ 34 | .vscode/* 35 | !.vscode/extensions.json 36 | !.vscode/settings.json 37 | !.vscode/tasks.json 38 | !.vscode/launch.json 39 | *.suo 40 | *.ntvs* 41 | *.njsproj 42 | *.sln 43 | *.sw? 44 | 45 | # macOS specific 46 | .DS_Store 47 | .AppleDouble 48 | .LSOverride 49 | 50 | # Motia specific 51 | .motia/cache/ 52 | 53 | # Python 54 | __pycache__/ 55 | *.py[cod] 56 | *$py.class 57 | *.so 58 | .Python 59 | env/ 60 | venv/ 61 | ENV/ 62 | .env 63 | .venv 64 | env.bak/ 65 | venv.bak/ 66 | .ipynb_checkpoints 67 | .pytest_cache/ 68 | .pnpm-lock.yaml 69 | 70 | # Misc 71 | .cache/ 72 | .temp/ 73 | 74 | .cursorrules 75 | .cursor/ 76 | .motia/ -------------------------------------------------------------------------------- /examples/finance-agent/.mermaid/finance-workflow.mmd: -------------------------------------------------------------------------------- 1 | flowchart TD 2 | classDef apiStyle fill:#f96,stroke:#333,stroke-width:2px,color:#fff 3 | classDef eventStyle fill:#69f,stroke:#333,stroke-width:2px,color:#fff 4 | classDef cronStyle fill:#9c6,stroke:#333,stroke-width:2px,color:#fff 5 | classDef noopStyle fill:#3f3a50,stroke:#333,stroke-width:2px,color:#fff 6 | steps_finance_data_step["⚡ FinanceDataAgent"]:::eventStyle 7 | steps_openai_analysis_step["⚡ OpenAIAnalysisHandler"]:::eventStyle 8 | steps_query_api_step["🌐 FinanceQueryAPI"]:::apiStyle 9 | steps_response_coordinator_step["⚡ ResponseCoordinator"]:::eventStyle 10 | steps_result_api_step["🌐 FinanceResultAPI"]:::apiStyle 11 | steps_save_result_step["⚡ SaveResultHandler"]:::eventStyle 12 | steps_web_search_step["⚡ WebSearchAgent"]:::eventStyle 13 | steps_finance_data_step -->|Finance data completed| steps_response_coordinator_step 14 | steps_openai_analysis_step -->|Analysis completed| steps_save_result_step 15 | steps_query_api_step -->|Query received| steps_finance_data_step 16 | steps_query_api_step -->|Query received| steps_web_search_step 17 | steps_response_coordinator_step -->|Response completed| steps_openai_analysis_step 18 | steps_result_api_step -->|Analysis completed| steps_save_result_step 19 | steps_web_search_step -->|Web search completed| steps_response_coordinator_step 20 | -------------------------------------------------------------------------------- /examples/finance-agent/docs/finance-agent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MotiaDev/motia-examples/c1ea1854d55a9d75218e6c00a8302dcb98289bb8/examples/finance-agent/docs/finance-agent.png -------------------------------------------------------------------------------- /examples/finance-agent/motia-workbench.json: -------------------------------------------------------------------------------- 1 | { 2 | "finance-workflow": { 3 | "steps/finance-data.step.ts": { 4 | "x": -140.4404554249536, 5 | "y": 151.73572674502785 6 | }, 7 | "steps/query-api.step.ts": { 8 | "x": 71.81990168760814, 9 | "y": -148.0437127839301 10 | }, 11 | "steps/web-search.step.ts": { 12 | "x": 441.7986444248934, 13 | "y": 160.0043970646082 14 | }, 15 | "steps/response-coordinator.step.ts": { 16 | "x": 117, 17 | "y": 412 18 | }, 19 | "steps/result-api.step.ts": { 20 | "x": 756.4594576203167, 21 | "y": 323.4567710695154 22 | }, 23 | "steps/openai-analysis.step.ts": { 24 | "x": 34.657504574538336, 25 | "y": 660.1251877657294 26 | }, 27 | "steps/save-result.step.ts": { 28 | "x": 287.0923054985997, 29 | "y": 942.0846317377112 30 | }, 31 | "steps/start.step.ts": { 32 | "x": 63.08973429888448, 33 | "y": -597.1535536683457 34 | } 35 | }, 36 | "main-flow": { 37 | "steps/start.step.ts": { 38 | "x": 0, 39 | "y": 0 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /examples/finance-agent/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "finance-agent", 3 | "description": "A finance agent that uses Motia to get financial data", 4 | "scripts": { 5 | "dev": "motia dev --verbose", 6 | "dev:debug": "motia dev --debug", 7 | "generate:config": "motia get-config --output ./" 8 | }, 9 | "keywords": [ 10 | "motia" 11 | ], 12 | "dependencies": { 13 | "@motiadev/core": "0.1.0-beta.15", 14 | "@motiadev/workbench": "0.1.0-beta.15", 15 | "alpha-vantage-cli": "^1.0.8", 16 | "axios": "^1.8.4", 17 | "dotenv": "^16.4.7", 18 | "motia": "0.1.0-beta.15", 19 | "openai": "^4.90.0", 20 | "react": "^19.0.0", 21 | "yahoo-finance2": "^2.13.3", 22 | "zod": "^3.24.2" 23 | }, 24 | "devDependencies": { 25 | "@types/react": "^19.0.12", 26 | "ts-node": "^10.9.2", 27 | "typescript": "^5.8.2" 28 | } 29 | } -------------------------------------------------------------------------------- /examples/finance-agent/steps/query-api.step.ts: -------------------------------------------------------------------------------- 1 | import { ApiRouteConfig, StepHandler } from 'motia'; 2 | import { z } from 'zod'; 3 | 4 | const bodySchema = z.object({ 5 | query: z.string().min(1, "Query must not be empty") 6 | }); 7 | 8 | export const config: ApiRouteConfig = { 9 | type: 'api', 10 | name: 'FinanceQueryAPI', 11 | description: 'Accepts financial analysis queries from users', 12 | path: '/finance-query', 13 | method: 'POST', 14 | virtualSubscribes: ['flow.started'], 15 | emits: [{ 16 | topic: 'query.received', 17 | label: 'Query received' 18 | }], 19 | bodySchema, 20 | flows: ['finance-workflow'] 21 | }; 22 | 23 | export const handler: StepHandler = async (req, { logger, emit, traceId }) => { 24 | logger.info('Finance query received', { query: req.body.query, traceId }); 25 | 26 | try { 27 | // Emit the received query event to start the workflow 28 | await emit({ 29 | topic: 'query.received', 30 | data: { 31 | query: req.body.query, 32 | timestamp: new Date().toISOString() 33 | } 34 | }); 35 | 36 | return { 37 | status: 200, 38 | body: { 39 | message: 'Query received and processing started', 40 | traceId 41 | } 42 | }; 43 | } catch (error: unknown) { 44 | const errorMessage = error instanceof Error ? error.message : String(error); 45 | logger.error('Error processing query', { error: errorMessage, traceId }); 46 | return { 47 | status: 500, 48 | body: { 49 | error: 'Failed to process query', 50 | message: errorMessage 51 | } 52 | }; 53 | } 54 | }; -------------------------------------------------------------------------------- /examples/finance-agent/steps/result-api.step.ts: -------------------------------------------------------------------------------- 1 | import { ApiRouteConfig, StepHandler } from 'motia'; 2 | 3 | export const config: ApiRouteConfig = { 4 | type: 'api', 5 | name: 'FinanceResultAPI', 6 | description: 'Retrieves the results of a financial analysis by trace ID', 7 | path: '/finance-result/:traceId', 8 | method: 'GET', 9 | emits: [{ 10 | topic: 'analysis.completed', 11 | label: 'Analysis completed' 12 | }], 13 | flows: ['finance-workflow'] 14 | }; 15 | 16 | export const handler: StepHandler = async (req, { logger, state }) => { 17 | const { traceId } = req.pathParams as { traceId: string }; 18 | logger.info(`Result retrieval requested ${traceId}`); 19 | 20 | try { 21 | // Get all response data from state using the trace ID from the path 22 | const responseData = await state.get(traceId, 'response.data'); 23 | 24 | if (!responseData) { 25 | logger.info('No results found for trace ID', { traceId }); 26 | return { 27 | status: 404, 28 | body: { 29 | error: 'No results found', 30 | message: 'No analysis results found for the provided trace ID' 31 | } 32 | }; 33 | } 34 | 35 | logger.info('Results retrieved successfully', { traceId }); 36 | return { 37 | status: 200, 38 | body: responseData 39 | }; 40 | 41 | } catch (error: unknown) { 42 | const errorMessage = error instanceof Error ? error.message : String(error); 43 | logger.error('Result retrieval failed', { error: errorMessage, traceId }); 44 | 45 | return { 46 | status: 500, 47 | body: { 48 | error: 'Failed to retrieve results', 49 | message: errorMessage 50 | } 51 | }; 52 | } 53 | }; -------------------------------------------------------------------------------- /examples/finance-agent/steps/save-result.step.ts: -------------------------------------------------------------------------------- 1 | import { EventConfig, StepHandler } from 'motia'; 2 | import { z } from 'zod'; 3 | 4 | const inputSchema = z.object({ 5 | query: z.string().optional(), 6 | timestamp: z.string(), 7 | response: z.any(), 8 | error: z.string().optional() 9 | }); 10 | 11 | export const config: EventConfig = { 12 | type: 'event', 13 | name: 'SaveResultHandler', 14 | description: 'Saves completed analysis results to state for later retrieval', 15 | subscribes: ['analysis.completed'], 16 | emits: ['result.saved'], 17 | input: inputSchema, 18 | flows: ['finance-workflow'] 19 | }; 20 | 21 | export const handler: StepHandler = async (input, { logger, state, traceId }) => { 22 | logger.info('Saving analysis result', { traceId }); 23 | 24 | try { 25 | // Store the full response data with analysis 26 | await state.set(traceId, 'response.data', { 27 | query: input.query, 28 | timestamp: input.timestamp, 29 | response: input.response, 30 | error: input.error, 31 | status: input.error ? 'error' : 'success' 32 | }); 33 | 34 | logger.info('Analysis result saved successfully', { traceId }); 35 | 36 | } catch (error: unknown) { 37 | const errorMessage = error instanceof Error ? error.message : String(error); 38 | logger.error('Failed to save analysis result', { error: errorMessage, traceId }); 39 | 40 | // Try to save the error information 41 | try { 42 | await state.set(traceId, 'response.data', { 43 | error: 'Failed to save result: ' + errorMessage, 44 | timestamp: new Date().toISOString(), 45 | status: 'error' 46 | }); 47 | } catch (storeError) { 48 | logger.error('Critical: Failed to save error information', { error: String(storeError) }); 49 | } 50 | } 51 | }; -------------------------------------------------------------------------------- /examples/finance-agent/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "resolveJsonModule": true, 11 | "allowJs": true, 12 | "outDir": "dist", 13 | "rootDir": ".", 14 | "baseUrl": ".", 15 | "jsx": "react-jsx" 16 | }, 17 | "include": [ 18 | "**/*.ts", 19 | "**/*.tsx", 20 | "**/*.js", 21 | "**/*.jsx" 22 | ], 23 | "exclude": [ 24 | "node_modules", 25 | "dist", 26 | "tests" 27 | ] 28 | } -------------------------------------------------------------------------------- /examples/github-integration-workflow/.env.example: -------------------------------------------------------------------------------- 1 | # GitHub Configuration 2 | GITHUB_TOKEN=your_github_token_here 3 | 4 | # OpenAI Configuration 5 | OPENAI_API_KEY=your_openai_api_key -------------------------------------------------------------------------------- /examples/github-integration-workflow/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:prettier/recommended', 7 | ], 8 | plugins: ['@typescript-eslint'], 9 | env: { 10 | node: true, 11 | es2022: true, 12 | }, 13 | parserOptions: { 14 | ecmaVersion: 2022, 15 | sourceType: 'module', 16 | }, 17 | rules: { 18 | '@typescript-eslint/explicit-function-return-type': 'off', 19 | '@typescript-eslint/no-explicit-any': 'warn', 20 | '@typescript-eslint/no-unused-vars': [ 21 | 'error', 22 | { 23 | argsIgnorePattern: '^_', 24 | varsIgnorePattern: '^_', 25 | }, 26 | ], 27 | 'no-console': 'warn', 28 | 'prettier/prettier': [ 29 | 'error', 30 | { 31 | singleQuote: true, 32 | semi: false, 33 | }, 34 | ], 35 | }, 36 | } 37 | -------------------------------------------------------------------------------- /examples/github-integration-workflow/.mermaid/github-issue-management.mmd: -------------------------------------------------------------------------------- 1 | flowchart TD 2 | classDef apiStyle fill:#f96,stroke:#333,stroke-width:2px,color:#fff 3 | classDef eventStyle fill:#69f,stroke:#333,stroke-width:2px,color:#fff 4 | classDef cronStyle fill:#9c6,stroke:#333,stroke-width:2px,color:#fff 5 | classDef noopStyle fill:#3f3a50,stroke:#333,stroke-width:2px,color:#fff 6 | steps_issue_triage_assignee_selector_step["⚡ Assignee Selector"]:::eventStyle 7 | steps_issue_triage_github_webhook_step["🌐 GitHub Webhook Handler"]:::apiStyle 8 | steps_issue_triage_handle_issue_closure_step["⚡ Issue Closure Handler"]:::eventStyle 9 | steps_issue_triage_handle_issue_update_step["⚡ Issue Update Handler"]:::eventStyle 10 | steps_issue_triage_handle_new_issue_step["⚡ New Issue Handler"]:::eventStyle 11 | steps_issue_triage_issue_classifier_step["⚡ Issue Classifier"]:::eventStyle 12 | steps_issue_triage_label_assigner_step["⚡ Label Assigner"]:::eventStyle 13 | steps_issue_triage_test_github_issue_step["⚙️ Test GitHub Issue"]:::noopStyle 14 | steps_issue_triage_github_webhook_step -->|New issue created| steps_issue_triage_handle_new_issue_step 15 | steps_issue_triage_github_webhook_step -->|Issue content updated| steps_issue_triage_handle_issue_update_step 16 | steps_issue_triage_github_webhook_step -->|Issue marked as closed| steps_issue_triage_handle_issue_closure_step 17 | steps_issue_triage_handle_new_issue_step -->|Initial processing complete| steps_issue_triage_issue_classifier_step 18 | steps_issue_triage_issue_classifier_step -->|Classification complete| steps_issue_triage_label_assigner_step 19 | steps_issue_triage_label_assigner_step -->|Labels applied to issue| steps_issue_triage_assignee_selector_step 20 | steps_issue_triage_test_github_issue_step -->|Simulate GitHub webhook| steps_issue_triage_github_webhook_step 21 | -------------------------------------------------------------------------------- /examples/github-integration-workflow/.mermaid/github-pr-management.mmd: -------------------------------------------------------------------------------- 1 | flowchart TD 2 | classDef apiStyle fill:#f96,stroke:#333,stroke-width:2px,color:#fff 3 | classDef eventStyle fill:#69f,stroke:#333,stroke-width:2px,color:#fff 4 | classDef cronStyle fill:#9c6,stroke:#333,stroke-width:2px,color:#fff 5 | classDef noopStyle fill:#3f3a50,stroke:#333,stroke-width:2px,color:#fff 6 | steps_pr_classifier_pr_classifier_step["⚡ PR Classifier"]:::eventStyle 7 | steps_pr_classifier_pr_label_assigner_step["⚡ PR Label Assigner"]:::eventStyle 8 | steps_pr_classifier_pr_reviewer_assigner_step["⚡ PR Reviewer Assigner"]:::eventStyle 9 | steps_pr_classifier_pr_test_monitor_step["⚡ PR Test Monitor"]:::eventStyle 10 | steps_pr_classifier_pr_webhook_step["🌐 PR Webhook Handler"]:::apiStyle 11 | steps_pr_classifier_test_pr_webhook_step["⚙️ Test PR Webhook"]:::noopStyle 12 | steps_pr_classifier_pr_classifier_step -->|PR classification complete| steps_pr_classifier_pr_label_assigner_step 13 | steps_pr_classifier_pr_classifier_step -->|PR classification complete| steps_pr_classifier_pr_reviewer_assigner_step 14 | steps_pr_classifier_pr_webhook_step -->|New PR created| steps_pr_classifier_pr_classifier_step 15 | steps_pr_classifier_pr_webhook_step -->|New PR created| steps_pr_classifier_pr_test_monitor_step 16 | steps_pr_classifier_pr_webhook_step -->|PR content updated| steps_pr_classifier_pr_test_monitor_step 17 | steps_pr_classifier_test_pr_webhook_step -->|Simulate PR webhook| steps_pr_classifier_pr_webhook_step 18 | -------------------------------------------------------------------------------- /examples/github-integration-workflow/.prettierignore: -------------------------------------------------------------------------------- 1 | # Build artifacts 2 | dist 3 | build 4 | coverage 5 | 6 | # Dependencies 7 | node_modules 8 | pnpm-lock.yaml 9 | 10 | # Generated files 11 | .motia 12 | 13 | # Misc 14 | .DS_Store 15 | .env 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local -------------------------------------------------------------------------------- /examples/github-integration-workflow/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5", 4 | "semi": false, 5 | "tabWidth": 2, 6 | "printWidth": 100, 7 | "bracketSpacing": true, 8 | "arrowParens": "avoid", 9 | "endOfLine": "lf" 10 | } 11 | -------------------------------------------------------------------------------- /examples/github-integration-workflow/README.md: -------------------------------------------------------------------------------- 1 | # GitHub Integration Workflow 2 | 3 | A comprehensive workflow for automating GitHub issue and pull request management using AI-powered classification and routing. 4 | 5 | ## Features 6 | 7 | - 🤖 Automated issue triage and classification 8 | - 🏷️ Intelligent label assignment 9 | - 👥 Smart assignee and reviewer selection 10 | - ✅ Automated PR test monitoring 11 | - 📝 Contextual comment generation 12 | 13 | ## Prerequisites 14 | 15 | - Node.js (v16 or higher) 16 | - GitHub account and personal access token 17 | - OpenAI API key 18 | 19 | ## Setup 20 | 21 | 1. Install dependencies: 22 | ```bash 23 | npm install 24 | ``` 25 | 2. Copy the environment template and add your credentials: 26 | ```bash 27 | cp .env.example .env 28 | ``` 29 | 3. Configure the following environment variables in the `.env` file: 30 | ```bash 31 | GITHUB_TOKEN=your_github_token_here 32 | OPENAI_API_KEY=your_openai_api_key 33 | ``` 34 | 35 | ## Development 36 | 37 | Start the development server: 38 | 39 | ```bash 40 | npm run dev 41 | ``` 42 | 43 | For debugging: 44 | 45 | ```bash 46 | npm run dev:debug 47 | ``` 48 | 49 | ## Testing 50 | 51 | Run tests: 52 | 53 | ```bash 54 | npm test 55 | ``` 56 | 57 | Watch mode: 58 | 59 | ```bash 60 | npm run test:watch 61 | ``` 62 | 63 | Generate coverage report: 64 | 65 | ```bash 66 | npm run test:coverage 67 | ``` 68 | 69 | ## Project Structure 70 | 71 | ``` 72 | ├── services/ 73 | │ ├── github/ # GitHub API integration 74 | │ └── openai/ # OpenAI API integration 75 | ├── steps/ 76 | │ ├── issue-triage/ # Issue management workflows 77 | │ └── pr-classifier/ # PR management workflows 78 | └── ... 79 | ``` 80 | 81 | ## Environment Variables 82 | 83 | The following environment variables are required for the application to function correctly: 84 | 85 | - `GITHUB_TOKEN`: Your GitHub personal access token. 86 | - `OPENAI_API_KEY`: Your OpenAI API key. 87 | -------------------------------------------------------------------------------- /examples/github-integration-workflow/__tests__/mocks/github-events.mock.ts: -------------------------------------------------------------------------------- 1 | import { PRClassification, IssueClassification } from '../../types/github' 2 | 3 | export const mockPROpenedEvent = { 4 | prNumber: 123, 5 | title: 'Add new feature for user authentication', 6 | body: 'This PR adds a new feature for user authentication using OAuth2.', 7 | owner: 'motia', 8 | repo: 'motia-examples', 9 | author: 'testuser', 10 | baseBranch: 'main', 11 | headBranch: 'feature/user-auth', 12 | commitSha: 'abc123def456', 13 | } 14 | 15 | export const mockPRClassification: PRClassification = { 16 | type: 'feature', 17 | impact: 'medium', 18 | areas: ['authentication', 'frontend', 'api'], 19 | } 20 | 21 | export const mockPRClassifiedEvent = { 22 | ...mockPROpenedEvent, 23 | classification: mockPRClassification, 24 | } 25 | 26 | export const mockIssueOpenedEvent = { 27 | issueNumber: 456, 28 | title: 'Bug: Login page crashes on mobile', 29 | body: 'When trying to login on mobile devices, the page crashes after entering credentials.', 30 | owner: 'motia', 31 | repo: 'motia-examples', 32 | author: 'testuser', 33 | } 34 | 35 | export const mockIssueClassification: IssueClassification = { 36 | type: 'bug', 37 | priority: 'high', 38 | complexity: 'moderate', 39 | } 40 | 41 | export const mockIssueClassifiedEvent = { 42 | ...mockIssueOpenedEvent, 43 | classification: mockIssueClassification, 44 | } 45 | 46 | export const mockTeamMembers = [ 47 | { 48 | login: 'frontend-dev', 49 | expertise: ['frontend', 'react', 'css'], 50 | }, 51 | { 52 | login: 'backend-dev', 53 | expertise: ['backend', 'api', 'database'], 54 | }, 55 | { 56 | login: 'security-expert', 57 | expertise: ['security', 'authentication', 'authorization'], 58 | }, 59 | ] 60 | 61 | export const mockCheckRuns = [ 62 | { 63 | id: 1, 64 | name: 'build', 65 | status: 'completed', 66 | conclusion: 'success', 67 | started_at: '2023-01-01T10:00:00Z', 68 | completed_at: '2023-01-01T10:05:00Z', 69 | }, 70 | { 71 | id: 2, 72 | name: 'test', 73 | status: 'completed', 74 | conclusion: 'success', 75 | started_at: '2023-01-01T10:06:00Z', 76 | completed_at: '2023-01-01T10:10:00Z', 77 | }, 78 | ] 79 | -------------------------------------------------------------------------------- /examples/github-integration-workflow/docs/images/github-issue-management.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MotiaDev/motia-examples/c1ea1854d55a9d75218e6c00a8302dcb98289bb8/examples/github-integration-workflow/docs/images/github-issue-management.png -------------------------------------------------------------------------------- /examples/github-integration-workflow/docs/images/github-pr-management.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MotiaDev/motia-examples/c1ea1854d55a9d75218e6c00a8302dcb98289bb8/examples/github-integration-workflow/docs/images/github-pr-management.png -------------------------------------------------------------------------------- /examples/github-integration-workflow/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | transform: { 5 | '^.+\\.[tj]sx?$': 'ts-jest', 6 | }, 7 | transformIgnorePatterns: ['/node_modules/(?!.*@motiadev)'], 8 | moduleNameMapper: { 9 | '^@motiadev/test(.*)$': '/node_modules/@motiadev/test/dist$1', 10 | '^motia(.*)$': '/node_modules/motia/dist$1', 11 | }, 12 | setupFiles: ['/jest.setup.js'], 13 | testMatch: ['**/__tests__/**/*.test.ts'], 14 | moduleFileExtensions: ['ts', 'js'], 15 | clearMocks: true, 16 | resetMocks: true, 17 | } 18 | -------------------------------------------------------------------------------- /examples/github-integration-workflow/jest.setup.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | require('dotenv').config({ 3 | path: path.resolve(__dirname, '.env.test'), 4 | }) 5 | 6 | process.env.NODE_ENV = 'test' 7 | -------------------------------------------------------------------------------- /examples/github-integration-workflow/motia-workbench.json: -------------------------------------------------------------------------------- 1 | { 2 | "github-issue-management": { 3 | "steps/issue-triage/assignee-selector.step.ts": { 4 | "x": 559.5, 5 | "y": 908 6 | }, 7 | "steps/issue-triage/github-webhook.step.ts": { 8 | "x": 266.5, 9 | "y": 176 10 | }, 11 | "steps/issue-triage/handle-issue-closure.step.ts": { 12 | "x": -329.7722758794514, 13 | "y": 377.03797356994505 14 | }, 15 | "steps/issue-triage/handle-issue-update.step.ts": { 16 | "x": 48.44849589790334, 17 | "y": 552.0619872379216 18 | }, 19 | "steps/issue-triage/handle-new-issue.step.ts": { 20 | "x": 558, 21 | "y": 380 22 | }, 23 | "steps/issue-triage/issue-classifier.step.ts": { 24 | "x": 351.5652401716595, 25 | "y": 564.6971993674764 26 | }, 27 | "steps/issue-triage/label-assigner.step.ts": { 28 | "x": 570.5, 29 | "y": 732 30 | }, 31 | "steps/issue-triage/test-github-issue.step.ts": { 32 | "x": 228.5, 33 | "y": 0 34 | } 35 | }, 36 | "github-pr-management": { 37 | "steps/pr-classifier/pr-classifier.step.ts": { 38 | "x": 55.9017775752051, 39 | "y": 556 40 | }, 41 | "steps/pr-classifier/pr-label-assigner.step.ts": { 42 | "x": -143.40929808568822, 43 | "y": 747.8505013673655 44 | }, 45 | "steps/pr-classifier/pr-reviewer-assigner.step.ts": { 46 | "x": 493.26709206927984, 47 | "y": 746.3409298085688 48 | }, 49 | "steps/pr-classifier/pr-test-monitor.step.ts": { 50 | "x": 508.88536918869636, 51 | "y": 551.4712853236099 52 | }, 53 | "steps/pr-classifier/pr-webhook.step.ts": { 54 | "x": 169.75, 55 | "y": 300 56 | }, 57 | "steps/pr-classifier/test-pr-webhook.step.ts": { 58 | "x": 202.75, 59 | "y": 0 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /examples/github-integration-workflow/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-integration-workflow", 3 | "description": "A workflow for integrating GitHub with Motia", 4 | "scripts": { 5 | "dev": "motia dev", 6 | "dev:debug": "motia dev --debug", 7 | "generate:config": "motia get-config --output ./", 8 | "test": "NODE_ENV=test jest", 9 | "test:watch": "NODE_ENV=test jest --watch", 10 | "test:coverage": "NODE_ENV=test jest --coverage", 11 | "test:coverage:report": "NODE_ENV=test jest --coverage && open coverage/lcov-report/index.html", 12 | "lint": "eslint . --ext .ts,.tsx", 13 | "lint:fix": "eslint . --ext .ts,.tsx --fix", 14 | "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"" 15 | }, 16 | "keywords": [ 17 | "motia" 18 | ], 19 | "dependencies": { 20 | "@octokit/rest": "^21.1.1", 21 | "dotenv": "^16.4.7", 22 | "express": "^4.21.2", 23 | "motia": "^0.1.0-beta.15", 24 | "openai": "^4.90.0", 25 | "react": "^19.0.0", 26 | "zod": "^3.24.2" 27 | }, 28 | "devDependencies": { 29 | "@motiadev/test": "^0.1.0-beta.15", 30 | "@types/jest": "^29.5.14", 31 | "@types/react": "^19.0.12", 32 | "jest": "^29.7.0", 33 | "ts-jest": "^29.3.0", 34 | "ts-node": "^10.9.2", 35 | "typescript": "^5.7.3", 36 | "@typescript-eslint/eslint-plugin": "^6.21.0", 37 | "@typescript-eslint/parser": "^6.21.0", 38 | "eslint": "^8.57.0", 39 | "eslint-config-prettier": "^9.1.0", 40 | "eslint-plugin-prettier": "^5.1.3", 41 | "prettier": "^3.2.5", 42 | "dotenv": "^16.4.5" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/github-integration-workflow/steps/issue-triage/github-webhook.step.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import { GithubIssueEvent, GithubWebhookEndpoint } from '../../types/github-events' 3 | import type { ApiRouteConfig, StepHandler } from 'motia' 4 | 5 | const webhookSchema = z.object({ 6 | action: z.string(), 7 | issue: z.object({ 8 | number: z.number(), 9 | title: z.string(), 10 | body: z.string().optional(), 11 | state: z.string(), 12 | labels: z.array(z.object({ name: z.string() })), 13 | }), 14 | repository: z.object({ 15 | owner: z.object({ login: z.string() }), 16 | name: z.string(), 17 | }), 18 | }) 19 | 20 | export const config: ApiRouteConfig = { 21 | type: 'api', 22 | name: 'GitHub Webhook Handler', 23 | path: GithubWebhookEndpoint.Issue, 24 | virtualSubscribes: [GithubWebhookEndpoint.Issue], 25 | method: 'POST', 26 | emits: [ 27 | { 28 | topic: GithubIssueEvent.Opened, 29 | label: 'New issue created', 30 | }, 31 | { 32 | topic: GithubIssueEvent.Edited, 33 | label: 'Issue content updated', 34 | }, 35 | { 36 | topic: GithubIssueEvent.Closed, 37 | label: 'Issue marked as closed', 38 | }, 39 | ], 40 | bodySchema: webhookSchema, 41 | flows: ['github-issue-management'], 42 | } 43 | 44 | export const handler: StepHandler = async (req, { emit, logger }) => { 45 | const { action, issue, repository } = req.body 46 | 47 | logger.info('[GitHub Webhook] Received webhook', { 48 | action, 49 | issueNumber: issue.number, 50 | }) 51 | 52 | await emit({ 53 | topic: `github.issue.${action}` as GithubIssueEvent, 54 | data: { 55 | issueNumber: issue.number, 56 | title: issue.title, 57 | body: issue.body, 58 | state: issue.state, 59 | labels: issue.labels.map((l: { name: string }) => l.name), 60 | owner: repository.owner.login, 61 | repo: repository.name, 62 | }, 63 | }) 64 | 65 | return { 66 | status: 200, 67 | body: { message: 'Webhook processed successfully' }, 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /examples/github-integration-workflow/steps/issue-triage/handle-issue-closure.step.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import { GithubClient } from '../../services/github/GithubClient' 3 | import { GithubIssueEvent } from '../../types/github-events' 4 | import type { EventConfig, StepHandler } from 'motia' 5 | 6 | const closureSchema = z.object({ 7 | issueNumber: z.number(), 8 | owner: z.string(), 9 | repo: z.string(), 10 | }) 11 | 12 | export const config: EventConfig = { 13 | type: 'event', 14 | name: 'Issue Closure Handler', 15 | description: 'Processes closed issues by adding closure labels and thank you comment', 16 | subscribes: [GithubIssueEvent.Closed], 17 | emits: [ 18 | { 19 | topic: GithubIssueEvent.Archived, 20 | label: 'Issue archived', 21 | }, 22 | ], 23 | input: closureSchema, 24 | flows: ['github-issue-management'], 25 | } 26 | 27 | export const handler: StepHandler = async (input, { emit, logger }) => { 28 | const github = new GithubClient() 29 | 30 | logger.info('[Issue Closure Handler] Processing issue closure', { 31 | issueNumber: input.issueNumber, 32 | }) 33 | 34 | try { 35 | await github.createComment( 36 | input.owner, 37 | input.repo, 38 | input.issueNumber, 39 | '🔒 This issue has been closed. Thank you for your contribution!' 40 | ) 41 | 42 | await github.addLabels(input.owner, input.repo, input.issueNumber, ['closed']) 43 | 44 | await emit({ 45 | topic: GithubIssueEvent.Archived, 46 | data: { 47 | issueNumber: input.issueNumber, 48 | status: 'closed', 49 | }, 50 | }) 51 | } catch (error) { 52 | logger.error('[Issue Closure Handler] Error processing closure', { 53 | error, 54 | issueNumber: input.issueNumber, 55 | }) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /examples/github-integration-workflow/steps/issue-triage/handle-issue-update.step.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import { GithubClient } from '../../services/github/GithubClient' 3 | import { GithubIssueEvent } from '../../types/github-events' 4 | import type { EventConfig, StepHandler } from 'motia' 5 | 6 | const updateSchema = z.object({ 7 | issueNumber: z.number(), 8 | title: z.string(), 9 | body: z.string().optional(), 10 | owner: z.string(), 11 | repo: z.string(), 12 | }) 13 | 14 | export const config: EventConfig = { 15 | type: 'event', 16 | name: 'Issue Update Handler', 17 | description: 'Handles issue updates by notifying reviewers of changes', 18 | subscribes: [GithubIssueEvent.Edited], 19 | emits: [ 20 | { 21 | topic: GithubIssueEvent.Updated, 22 | label: 'Update processed', 23 | }, 24 | ], 25 | input: updateSchema, 26 | flows: ['github-issue-management'], 27 | } 28 | 29 | export const handler: StepHandler = async (input, { emit, logger }) => { 30 | const github = new GithubClient() 31 | 32 | logger.info('[Issue Update Handler] Processing issue update', { 33 | issueNumber: input.issueNumber, 34 | }) 35 | 36 | try { 37 | await github.createComment( 38 | input.owner, 39 | input.repo, 40 | input.issueNumber, 41 | '📝 This issue has been updated. Our team will review the changes.' 42 | ) 43 | 44 | await emit({ 45 | topic: GithubIssueEvent.Updated, 46 | data: { 47 | issueNumber: input.issueNumber, 48 | status: 'updated', 49 | }, 50 | }) 51 | } catch (error) { 52 | logger.error('[Issue Update Handler] Error processing update', { 53 | error, 54 | issueNumber: input.issueNumber, 55 | }) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /examples/github-integration-workflow/steps/issue-triage/handle-new-issue.step.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import { GithubClient } from '../../services/github/GithubClient' 3 | import { GithubIssueEvent } from '../../types/github-events' 4 | import type { EventConfig, StepHandler } from 'motia' 5 | 6 | const issueSchema = z.object({ 7 | issueNumber: z.number(), 8 | title: z.string(), 9 | body: z.string().optional(), 10 | owner: z.string(), 11 | repo: z.string(), 12 | }) 13 | 14 | export const config: EventConfig = { 15 | type: 'event', 16 | name: 'New Issue Handler', 17 | description: 'Processes newly created issues by adding initial labels and welcome comment', 18 | subscribes: [GithubIssueEvent.Opened], 19 | emits: [ 20 | { 21 | topic: GithubIssueEvent.Processed, 22 | label: 'Initial processing complete', 23 | }, 24 | ], 25 | input: issueSchema, 26 | flows: ['github-issue-management'], 27 | } 28 | 29 | export const handler: StepHandler = async (input, { emit, logger }) => { 30 | const github = new GithubClient() 31 | 32 | logger.info('[New Issue Handler] Processing new issue', { 33 | issueNumber: input.issueNumber, 34 | }) 35 | 36 | try { 37 | await github.addLabels(input.owner, input.repo, input.issueNumber, ['triage-needed']) 38 | 39 | await github.createComment( 40 | input.owner, 41 | input.repo, 42 | input.issueNumber, 43 | '👋 Thanks for opening this issue! Our team will review it shortly.' 44 | ) 45 | 46 | await emit({ 47 | topic: GithubIssueEvent.Processed, 48 | data: { 49 | ...input, 50 | status: 'triaged', 51 | }, 52 | }) 53 | } catch (error) { 54 | logger.error('[New Issue Handler] Error processing issue', { 55 | error, 56 | issueNumber: input.issueNumber, 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /examples/github-integration-workflow/steps/issue-triage/issue-classifier.step.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import { OpenAIClient } from '../../services/openai/OpenAIClient' 3 | import { GithubIssueEvent } from '../../types/github-events' 4 | import type { EventConfig, StepHandler } from 'motia' 5 | 6 | const issueSchema = z.object({ 7 | issueNumber: z.number(), 8 | title: z.string(), 9 | body: z.string().optional(), 10 | owner: z.string(), 11 | repo: z.string(), 12 | }) 13 | 14 | export const config: EventConfig = { 15 | type: 'event', 16 | name: 'Issue Classifier', 17 | description: 'Uses LLM to classify GitHub issues', 18 | subscribes: [GithubIssueEvent.Processed], 19 | emits: [ 20 | { 21 | topic: GithubIssueEvent.Classified, 22 | label: 'Classification complete', 23 | }, 24 | ], 25 | input: issueSchema, 26 | flows: ['github-issue-management'], 27 | } 28 | 29 | export const handler: StepHandler = async (input, { emit, logger }) => { 30 | const openai = new OpenAIClient() 31 | 32 | logger.info('[Issue Classifier] Analyzing issue', { 33 | issueNumber: input.issueNumber, 34 | }) 35 | 36 | try { 37 | const classification = await openai.classifyIssue(input.title, input.body || '') 38 | 39 | logger.info('[Issue Classifier] Classification complete', { 40 | issueNumber: input.issueNumber, 41 | classification, 42 | }) 43 | 44 | await emit({ 45 | topic: GithubIssueEvent.Classified, 46 | data: { 47 | ...input, 48 | classification, 49 | }, 50 | }) 51 | } catch (error) { 52 | logger.error('[Issue Classifier] Classification failed', { 53 | error, 54 | issueNumber: input.issueNumber, 55 | }) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /examples/github-integration-workflow/steps/issue-triage/label-assigner.step.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import { GithubClient } from '../../services/github/GithubClient' 3 | import { GithubIssueEvent } from '../../types/github-events' 4 | import type { EventConfig, StepHandler } from 'motia' 5 | 6 | const classifiedIssueSchema = z.object({ 7 | issueNumber: z.number(), 8 | owner: z.string(), 9 | repo: z.string(), 10 | classification: z.object({ 11 | type: z.enum(['bug', 'feature', 'question', 'documentation']), 12 | priority: z.enum(['low', 'medium', 'high', 'critical']), 13 | complexity: z.enum(['simple', 'moderate', 'complex']), 14 | }), 15 | }) 16 | 17 | export const config: EventConfig = { 18 | type: 'event', 19 | name: 'Label Assigner', 20 | description: 'Assigns labels based on issue classification', 21 | subscribes: [GithubIssueEvent.Classified], 22 | emits: [ 23 | { 24 | topic: GithubIssueEvent.Labeled, 25 | label: 'Labels applied to issue', 26 | }, 27 | ], 28 | input: classifiedIssueSchema, 29 | flows: ['github-issue-management'], 30 | } 31 | 32 | export const handler: StepHandler = async (input, { emit, logger }) => { 33 | const github = new GithubClient() 34 | 35 | logger.info('[Label Assigner] Assigning labels', { 36 | issueNumber: input.issueNumber, 37 | classification: input.classification, 38 | }) 39 | 40 | try { 41 | const labels = [ 42 | `type:${input.classification.type}`, 43 | `priority:${input.classification.priority}`, 44 | `complexity:${input.classification.complexity}`, 45 | ] 46 | 47 | await github.addLabels(input.owner, input.repo, input.issueNumber, labels) 48 | 49 | await emit({ 50 | topic: GithubIssueEvent.Labeled, 51 | data: { 52 | ...input, 53 | labels, 54 | }, 55 | }) 56 | } catch (error) { 57 | logger.error('[Label Assigner] Failed to assign labels', { 58 | error, 59 | issueNumber: input.issueNumber, 60 | }) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /examples/github-integration-workflow/steps/issue-triage/test-github-issue.step.ts: -------------------------------------------------------------------------------- 1 | import { GithubWebhookEndpoint } from '../../types/github-events' 2 | import type { NoopConfig } from 'motia' 3 | 4 | export const config: NoopConfig = { 5 | type: 'noop', 6 | name: 'Test GitHub Issue', 7 | description: 'Simulates GitHub issue events for testing', 8 | virtualEmits: [ 9 | { 10 | topic: GithubWebhookEndpoint.Issue, 11 | label: 'Simulate GitHub webhook', 12 | }, 13 | ], 14 | virtualSubscribes: [], 15 | flows: ['github-issue-management'], 16 | } 17 | -------------------------------------------------------------------------------- /examples/github-integration-workflow/steps/issue-triage/test-github-issue.step.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { BaseHandle, Position } from 'motia/workbench' 3 | 4 | export default function TestGithubIssue() { 5 | const sendWebhook = (action: 'opened' | 'edited' | 'closed') => { 6 | const issueNumber = Math.floor(Math.random() * 1000) 7 | 8 | fetch('/api/github/webhook', { 9 | method: 'POST', 10 | headers: { 11 | 'Content-Type': 'application/json', 12 | }, 13 | body: JSON.stringify({ 14 | action, 15 | issue: { 16 | number: issueNumber, 17 | title: `Test Issue #${issueNumber}`, 18 | body: `This is a test issue for action: ${action}`, 19 | state: action === 'closed' ? 'closed' : 'open', 20 | labels: [], 21 | }, 22 | repository: { 23 | owner: { login: 'test-owner' }, 24 | name: 'test-repo', 25 | }, 26 | }), 27 | }) 28 | } 29 | 30 | return ( 31 |
32 |
GitHub Issue Simulator
33 |
34 | 40 | 41 | 47 | 48 | 54 |
55 | 56 |
57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /examples/github-integration-workflow/steps/pr-classifier/pr-classifier.step.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import { OpenAIClient } from '../../services/openai/OpenAIClient' 3 | import { GithubPREvent } from '../../types/github-events' 4 | import type { EventConfig, StepHandler } from 'motia' 5 | 6 | const prSchema = z.object({ 7 | prNumber: z.number(), 8 | title: z.string(), 9 | body: z.string().optional(), 10 | owner: z.string(), 11 | repo: z.string(), 12 | author: z.string(), 13 | baseBranch: z.string(), 14 | headBranch: z.string(), 15 | commitSha: z.string(), 16 | }) 17 | 18 | export const config: EventConfig = { 19 | type: 'event', 20 | name: 'PR Classifier', 21 | description: 'Uses LLM to classify PRs by type and impact', 22 | subscribes: [GithubPREvent.Opened], 23 | emits: [ 24 | { 25 | topic: GithubPREvent.Classified, 26 | label: 'PR classification complete', 27 | }, 28 | ], 29 | input: prSchema, 30 | flows: ['github-pr-management'], 31 | } 32 | 33 | export const handler: StepHandler = async (input, { emit, logger }) => { 34 | const openai = new OpenAIClient() 35 | 36 | logger.info('[PR Classifier] Analyzing PR', { 37 | prNumber: input.prNumber, 38 | }) 39 | 40 | try { 41 | const classification = await openai.classifyPR(input.title, input.body || '') 42 | 43 | logger.info('[PR Classifier] Classification complete', { 44 | prNumber: input.prNumber, 45 | classification, 46 | }) 47 | 48 | await emit({ 49 | topic: GithubPREvent.Classified, 50 | data: { 51 | ...input, 52 | classification, 53 | }, 54 | }) 55 | } catch (error) { 56 | logger.error('[PR Classifier] Classification failed', { 57 | error, 58 | prNumber: input.prNumber, 59 | }) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /examples/github-integration-workflow/steps/pr-classifier/pr-label-assigner.step.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import { GithubClient } from '../../services/github/GithubClient' 3 | import { GithubPREvent } from '../../types/github-events' 4 | import type { EventConfig, StepHandler } from 'motia' 5 | 6 | const classifiedPRSchema = z.object({ 7 | prNumber: z.number(), 8 | title: z.string(), 9 | body: z.string().optional(), 10 | owner: z.string(), 11 | repo: z.string(), 12 | author: z.string(), 13 | classification: z.object({ 14 | type: z.enum(['bug-fix', 'feature', 'documentation', 'refactor']), 15 | impact: z.enum(['low', 'medium', 'high']), 16 | areas: z.array(z.string()), 17 | }), 18 | }) 19 | 20 | export const config: EventConfig = { 21 | type: 'event', 22 | name: 'PR Label Assigner', 23 | description: 'Assigns labels to PRs based on LLM classification', 24 | subscribes: [GithubPREvent.Classified], 25 | emits: [ 26 | { 27 | topic: GithubPREvent.Labeled, 28 | label: 'Labels applied to PR', 29 | }, 30 | ], 31 | input: classifiedPRSchema, 32 | flows: ['github-pr-management'], 33 | } 34 | 35 | export const handler: StepHandler = async (input, { emit, logger }) => { 36 | const github = new GithubClient() 37 | 38 | logger.info('[PR Label Assigner] Assigning labels', { 39 | prNumber: input.prNumber, 40 | classification: input.classification, 41 | }) 42 | 43 | try { 44 | const labels = [ 45 | `type:${input.classification.type}`, 46 | `impact:${input.classification.impact}`, 47 | ...input.classification.areas.map((area: string) => `area:${area}`), 48 | ] 49 | 50 | await github.addLabels(input.owner, input.repo, input.prNumber, labels) 51 | 52 | await github.createComment( 53 | input.owner, 54 | input.repo, 55 | input.prNumber, 56 | `🏷️ Based on the PR analysis, I've added the following labels:\n${labels.map(l => `- \`${l}\``).join('\n')}` 57 | ) 58 | 59 | await emit({ 60 | topic: GithubPREvent.Labeled, 61 | data: { 62 | ...input, 63 | labels, 64 | }, 65 | }) 66 | } catch (error) { 67 | logger.error('[PR Label Assigner] Failed to assign labels', { 68 | error, 69 | prNumber: input.prNumber, 70 | }) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /examples/github-integration-workflow/steps/pr-classifier/test-pr-webhook.step.ts: -------------------------------------------------------------------------------- 1 | import { GithubWebhookEndpoint } from '../../types/github-events' 2 | import type { NoopConfig } from 'motia' 3 | 4 | export const config: NoopConfig = { 5 | type: 'noop', 6 | name: 'Test PR Webhook', 7 | description: 'Simulates GitHub PR webhook events for testing', 8 | virtualEmits: [ 9 | { 10 | topic: GithubWebhookEndpoint.PR, 11 | label: 'Simulate PR webhook', 12 | }, 13 | ], 14 | virtualSubscribes: [], 15 | flows: ['github-pr-management'], 16 | } 17 | -------------------------------------------------------------------------------- /examples/github-integration-workflow/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "resolveJsonModule": true, 11 | "allowJs": true, 12 | "outDir": "dist", 13 | "rootDir": ".", 14 | "baseUrl": ".", 15 | "jsx": "react-jsx" 16 | }, 17 | "include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"], 18 | "exclude": ["node_modules", "dist", "__tests__"] 19 | } 20 | -------------------------------------------------------------------------------- /examples/github-integration-workflow/types/github-events.ts: -------------------------------------------------------------------------------- 1 | import { IssueClassification } from './github' 2 | 3 | export enum GithubPREvent { 4 | Opened = 'github.pr.opened', 5 | Edited = 'github.pr.edited', 6 | Closed = 'github.pr.closed', 7 | Merged = 'github.pr.merged', 8 | Classified = 'github.pr.classified', 9 | Labeled = 'github.pr.labeled', 10 | ReviewersAssigned = 'github.pr.reviewers-assigned', 11 | TestsCompleted = 'github.pr.tests-completed', 12 | } 13 | 14 | export enum GithubIssueEvent { 15 | Opened = 'github.issue.opened', 16 | Edited = 'github.issue.edited', 17 | Closed = 'github.issue.closed', 18 | Processed = 'github.issue.processed', 19 | Classified = 'github.issue.classified', 20 | Labeled = 'github.issue.labeled', 21 | Assigned = 'github.issue.assigned', 22 | Updated = 'github.issue.updated', 23 | Archived = 'github.issue.archived', 24 | } 25 | 26 | export enum GithubWebhookEndpoint { 27 | Issue = '/api/github/webhook', 28 | PR = '/api/github/pr-webhook', 29 | } 30 | 31 | /** 32 | * Base interface for GitHub issue events 33 | */ 34 | export interface GithubIssueBaseEvent { 35 | issueNumber: number 36 | title: string 37 | body: string 38 | owner: string 39 | repo: string 40 | author: string 41 | state?: string 42 | labels?: string[] 43 | } 44 | 45 | /** 46 | * Event emitted when a GitHub issue is opened 47 | */ 48 | export type GithubIssueOpenedEvent = GithubIssueBaseEvent 49 | 50 | /** 51 | * Event emitted when a GitHub issue is edited 52 | */ 53 | export type GithubIssueEditedEvent = GithubIssueBaseEvent 54 | 55 | /** 56 | * Event emitted when a GitHub issue is closed 57 | */ 58 | export type GithubIssueClosedEvent = GithubIssueBaseEvent 59 | 60 | /** 61 | * Event emitted when a GitHub issue is classified 62 | */ 63 | export interface GithubIssueClassifiedEvent extends GithubIssueBaseEvent { 64 | classification: IssueClassification 65 | } 66 | 67 | /** 68 | * Event emitted when assignees are suggested for a GitHub issue 69 | */ 70 | export interface GithubIssueSuggestedAssigneesEvent extends GithubIssueClassifiedEvent { 71 | suggestedAssignees: string[] 72 | } 73 | 74 | /** 75 | * Event emitted when a GitHub issue is assigned 76 | */ 77 | export interface GithubIssueAssignedEvent extends GithubIssueClassifiedEvent { 78 | assignees: string[] 79 | } 80 | -------------------------------------------------------------------------------- /examples/github-integration-workflow/types/github-pr-events.ts: -------------------------------------------------------------------------------- 1 | export type GithubPREventTypes = { 2 | // Webhook events 3 | '/api/github/pr-webhook': 'PR webhook received' 4 | 5 | // PR lifecycle events 6 | 'github.pr.opened': 'New PR created' 7 | 'github.pr.edited': 'PR updated' 8 | 'github.pr.closed': 'PR closed' 9 | 'github.pr.merged': 'PR merged' 10 | 11 | // Processing events 12 | 'github.pr.processed': 'Initial PR processing complete' 13 | 'github.pr.classified': 'PR classified by LLM' 14 | 'github.pr.labeled': 'PR labels applied' 15 | 'github.pr.reviewers-assigned': 'PR reviewers assigned' 16 | 'github.pr.tests-completed': 'PR tests finished' 17 | 'github.pr.approved': 'PR approved' 18 | 'github.pr.ready-to-merge': 'PR ready for merging' 19 | 'github.pr.post-merge': 'Post-merge actions triggered' 20 | } 21 | -------------------------------------------------------------------------------- /examples/github-integration-workflow/types/github.ts: -------------------------------------------------------------------------------- 1 | export interface TeamMember { 2 | login: string 3 | expertise: string[] 4 | } 5 | 6 | export interface PRClassification { 7 | type: 'bug-fix' | 'feature' | 'documentation' | 'refactor' 8 | impact: 'low' | 'medium' | 'high' 9 | areas: string[] 10 | } 11 | 12 | export interface IssueClassification { 13 | type: 'bug' | 'feature' | 'question' | 'documentation' 14 | priority: 'low' | 'medium' | 'high' | 'critical' 15 | complexity: 'simple' | 'moderate' | 'complex' 16 | } 17 | 18 | export interface CheckRun { 19 | id: number 20 | name: string 21 | status: 'queued' | 'in_progress' | 'completed' 22 | conclusion: 23 | | 'success' 24 | | 'failure' 25 | | 'neutral' 26 | | 'cancelled' 27 | | 'skipped' 28 | | 'timed_out' 29 | | 'action_required' 30 | started_at: string 31 | completed_at?: string 32 | } 33 | 34 | export interface RawCheckRun { 35 | id: number 36 | name: string 37 | node_id: string 38 | external_id: string | null 39 | url: string 40 | html_url: string | null 41 | details_url: string | null 42 | head_sha: string 43 | status: 'queued' | 'in_progress' | 'completed' 44 | conclusion: 45 | | 'success' 46 | | 'failure' 47 | | 'neutral' 48 | | 'cancelled' 49 | | 'skipped' 50 | | 'timed_out' 51 | | 'action_required' 52 | | null 53 | started_at: string | null 54 | completed_at: string | null 55 | output: { 56 | title: string | null 57 | summary: string | null 58 | text: string | null 59 | annotations_count: number 60 | annotations_url: string 61 | } 62 | check_suite: { 63 | id: number 64 | } | null 65 | } 66 | -------------------------------------------------------------------------------- /examples/gmail-workflow/.env.example: -------------------------------------------------------------------------------- 1 | # Hugging Face API token 2 | HUGGINGFACE_API_TOKEN=your_huggingface_token 3 | 4 | # Discord webhook for daily summary 5 | DISCORD_WEBHOOK_URL=your_discord_webhook_url 6 | 7 | # Google OAuth2 credentials # andersonofl@gmail.com 8 | GOOGLE_CLIENT_ID=your_google_client_id 9 | GOOGLE_CLIENT_SECRET=your_google_client_secret 10 | GOOGLE_PUBSUB_TOPIC=your_google_topic_name 11 | GOOGLE_REDIRECT_URI=your_google_redirect_uri 12 | 13 | # Auto responder name and email 14 | AUTO_RESPONDER_NAME=your_name 15 | AUTO_RESPONDER_EMAIL=your_email 16 | -------------------------------------------------------------------------------- /examples/gmail-workflow/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | .pnpm-store/ 4 | .npm 5 | package-lock.json 6 | yarn.lock 7 | pnpm-lock.yaml 8 | .yarn/cache 9 | .yarn/unplugged 10 | .yarn/build-state.yml 11 | .yarn/install-state.gz 12 | 13 | coverage/ 14 | 15 | # Environment and secrets 16 | .env 17 | .env.* 18 | !.env.example 19 | .cursorrules 20 | 21 | # Motia generated 22 | .motia/ 23 | .idea/ 24 | 25 | # Python cache files 26 | */__pycache__/* 27 | *.py[cod] 28 | *.pyo 29 | 30 | # macOS system files 31 | .DS_Store 32 | .DS_Store? 33 | ._* 34 | .Spotlight-V100 35 | .Trashes -------------------------------------------------------------------------------- /examples/gmail-workflow/config/default.ts: -------------------------------------------------------------------------------- 1 | import { env } from "./env"; 2 | 3 | export const appConfig = { 4 | discord: { 5 | webhookUrl: env.DISCORD_WEBHOOK_URL 6 | }, 7 | google: { 8 | clientId: env.GOOGLE_CLIENT_ID, 9 | clientSecret: env.GOOGLE_CLIENT_SECRET, 10 | redirectUri: env.GOOGLE_REDIRECT_URI, 11 | topicName: env.GOOGLE_PUBSUB_TOPIC 12 | }, 13 | autoResponder: { 14 | name: env.AUTO_RESPONDER_NAME, 15 | email: env.AUTO_RESPONDER_EMAIL 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/gmail-workflow/config/env.ts: -------------------------------------------------------------------------------- 1 | import { cleanEnv, str } from 'envalid' 2 | 3 | export const env = cleanEnv(process.env, { 4 | DISCORD_WEBHOOK_URL: str(), 5 | GOOGLE_CLIENT_ID: str(), 6 | GOOGLE_CLIENT_SECRET: str(), 7 | GOOGLE_REDIRECT_URI: str({ devDefault: 'http://localhost:3000/api/auth/callback' }), 8 | GOOGLE_PUBSUB_TOPIC: str(), 9 | AUTO_RESPONDER_NAME: str(), 10 | AUTO_RESPONDER_EMAIL: str(), 11 | }) 12 | -------------------------------------------------------------------------------- /examples/gmail-workflow/docs/images/gmail-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MotiaDev/motia-examples/c1ea1854d55a9d75218e6c00a8302dcb98289bb8/examples/gmail-workflow/docs/images/gmail-flow.png -------------------------------------------------------------------------------- /examples/gmail-workflow/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | roots: [''], 5 | testMatch: ['**/*.test.ts'], 6 | transform: { 7 | '^.+\\.tsx?$': 'ts-jest', 8 | }, 9 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 10 | collectCoverage: true, 11 | coverageDirectory: 'coverage', 12 | collectCoverageFrom: ['**/*.ts'], 13 | moduleNameMapper: { 14 | '^@/(.*)$': '/$1' 15 | } 16 | }; -------------------------------------------------------------------------------- /examples/gmail-workflow/motia-workbench.json: -------------------------------------------------------------------------------- 1 | { 2 | "gmail-flow": { 3 | "steps/analyze-email.step.py": { 4 | "x": 481.2609021289569, 5 | "y": 799.8959507662372 6 | }, 7 | "steps/auto-responder.step.ts": { 8 | "x": 94.58234276582252, 9 | "y": 1109.3407506945664 10 | }, 11 | "steps/daily-summary.step.ts": { 12 | "x": 318.96515018157595, 13 | "y": 134.49762363338988 14 | }, 15 | "steps/fetch-email.step.ts": { 16 | "x": 480.5, 17 | "y": 632 18 | }, 19 | "steps/gmail-auth.step.ts": { 20 | "x": 820, 21 | "y": 58 22 | }, 23 | "steps/gmail-get-auth-url.step.ts": { 24 | "x": 1230, 25 | "y": 68 26 | }, 27 | "steps/gmail-token-status.step.ts": { 28 | "x": 1787, 29 | "y": 17.797480807096377 30 | }, 31 | "steps/gmail-watch.step.ts": { 32 | "x": 1498, 33 | "y": 68 34 | }, 35 | "steps/gmail-webhook.step.ts": { 36 | "x": 410, 37 | "y": 374 38 | }, 39 | "steps/noops/gmail-webhook-simulator.step.ts": { 40 | "x": -122.10120649650784, 41 | "y": 135.67495584589386 42 | }, 43 | "steps/noops/google-auth-noops.step.ts": { 44 | "x": 1178.1595872017188, 45 | "y": 549.9212499730157 46 | }, 47 | "steps/noops/human-review.step.ts": { 48 | "x": 390.3608084783237, 49 | "y": 1051.346312088437 50 | }, 51 | "steps/organize-email.step.ts": { 52 | "x": 820.5721410201845, 53 | "y": 1117.9755567072107 54 | }, 55 | "steps/api/gmail-get-auth-url.step.ts": { 56 | "x": 1123.2336682640073, 57 | "y": 144.74829385747896 58 | }, 59 | "steps/api/gmail-auth.step.ts": { 60 | "x": 821.0995361651217, 61 | "y": -68.13826775439205 62 | }, 63 | "steps/api/gmail-watch.step.ts": { 64 | "x": 1694.0415650472407, 65 | "y": 160.53102338726896 66 | }, 67 | "steps/api/gmail-webhook.step.ts": { 68 | "x": 386.148160906303, 69 | "y": 365.2941041301467 70 | }, 71 | "steps/api/gmail-token-status.step.ts": { 72 | "x": 1379.975989831029, 73 | "y": 156.59546900519388 74 | }, 75 | "steps/api/gmail-auth-callback.step.ts": { 76 | "x": 729.7900169482848, 77 | "y": 139.04916367932137 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /examples/gmail-workflow/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "google-gmail-workflow", 3 | "description": "A workflow for Google Gmail", 4 | "scripts": { 5 | "dev": "motia dev", 6 | "dev:debug": "motia dev --debug", 7 | "generate:config": "motia get-config --output ./", 8 | "test": "jest", 9 | "test:watch": "jest --watch" 10 | }, 11 | "keywords": [ 12 | "motia" 13 | ], 14 | "dependencies": { 15 | "@motiadev/core": "^0.1.0-beta.15", 16 | "@motiadev/workbench": "^0.1.0-beta.15", 17 | "axios": "^1.8.2", 18 | "envalid": "^8.0.0", 19 | "gmail-api-parse-message-ts": "^2.2.33", 20 | "google-auth-library": "^9.15.1", 21 | "googleapis": "^146.0.0", 22 | "motia": "^0.1.0-beta.15", 23 | "react": "^19.0.0", 24 | "zod": "^3.24.2" 25 | }, 26 | "devDependencies": { 27 | "@types/jest": "^29.5.14", 28 | "@types/node": "^22.13.10", 29 | "@types/react": "^19.0.10", 30 | "jest": "^29.7.0", 31 | "ts-jest": "^29.2.6", 32 | "ts-node": "^10.9.2", 33 | "typescript": "^5.8.2" 34 | } 35 | } -------------------------------------------------------------------------------- /examples/gmail-workflow/requirements.txt: -------------------------------------------------------------------------------- 1 | transformers==4.30.2 2 | torch==2.0.1 3 | numpy==1.24.3 4 | scikit-learn==1.2.2 5 | pandas==2.0.2 6 | python-dotenv==1.0.0 7 | huggingface_hub>=0.29.1 8 | requests>=2.32.3 9 | pyyaml>=6.0.2 10 | tqdm>=4.67.1 -------------------------------------------------------------------------------- /examples/gmail-workflow/services/state.service.ts: -------------------------------------------------------------------------------- 1 | import { FlowContext } from "@motiadev/core"; 2 | import { Credentials } from "google-auth-library"; 3 | 4 | export class StateService { 5 | private state: FlowContext['state'] 6 | 7 | constructor(state: FlowContext['state']) { 8 | this.state = state 9 | } 10 | 11 | async getState() { 12 | return this.state 13 | } 14 | 15 | async saveTokens(tokens: Credentials) { 16 | await this.state.set('gmail.auth', 'tokens', tokens) 17 | } 18 | 19 | async getTokens(): Promise { 20 | return this.state.get('gmail.auth', 'tokens') 21 | } 22 | 23 | async saveLastHistoryId(historyId: string) { 24 | await this.state.set('gmail.auth', 'lastHistoryId', historyId) 25 | } 26 | 27 | async getLastHistoryId(): Promise { 28 | return this.state.get('gmail.auth', 'lastHistoryId') 29 | } 30 | } -------------------------------------------------------------------------------- /examples/gmail-workflow/steps/api/gmail-auth-callback.step.ts: -------------------------------------------------------------------------------- 1 | import {ApiRouteConfig, StepHandler} from 'motia'; 2 | import { GoogleService } from '../../services/google.service'; 3 | 4 | export const config: ApiRouteConfig = { 5 | type: 'api', 6 | name: 'Gmail Auth', 7 | description: 'Handles OAuth2 callback from Google to complete Gmail authentication flow', 8 | path: '/api/auth/callback', 9 | method: 'GET', 10 | emits: ['gmail.auth'], 11 | flows: ['gmail-flow'], 12 | } 13 | 14 | export const handler: StepHandler = async (req, {logger, state}) => { 15 | const {code} = req.queryParams 16 | 17 | logger.info(`Received OAuth2 callback with code ${code}`) 18 | 19 | const googleService = new GoogleService(logger, state) 20 | 21 | try { 22 | await googleService.fetchTokens(code as string) 23 | } catch (error) { 24 | logger.error(`Error fetching tokens: ${error}`) 25 | return { 26 | status: 500, 27 | body: { 28 | message: 'Error fetching tokens' 29 | } 30 | } 31 | } 32 | 33 | return { 34 | status: 200, 35 | body: { 36 | message: 'Successfully authenticated' 37 | }, 38 | } 39 | } -------------------------------------------------------------------------------- /examples/gmail-workflow/steps/api/gmail-get-auth-url.step.ts: -------------------------------------------------------------------------------- 1 | import {ApiRouteConfig, StepHandler} from 'motia'; 2 | import { GoogleService } from '../../services/google.service'; 3 | 4 | export const config: ApiRouteConfig = { 5 | type: 'api', 6 | name: 'Gmail Get Auth URL', 7 | description: 'Get the auth URL for Gmail', 8 | path: '/api/get-auth-url', 9 | method: 'GET', 10 | emits: ['gmail.auth-url'], 11 | flows: ['gmail-flow'], 12 | } 13 | 14 | export const handler: StepHandler = async (_, {logger, state}) => { 15 | const googleService = new GoogleService(logger, state) 16 | 17 | try { 18 | const authUrl = await googleService.getAuthUrl() 19 | return { 20 | status: 200, 21 | body: { 22 | authUrl 23 | }, 24 | } 25 | } catch (error) { 26 | logger.error(`Error fetching tokens: ${error}`) 27 | return { 28 | status: 500, 29 | body: { 30 | message: 'Error fetching tokens' 31 | } 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /examples/gmail-workflow/steps/api/gmail-token-status.step.ts: -------------------------------------------------------------------------------- 1 | import {ApiRouteConfig, StepHandler} from 'motia'; 2 | import { GoogleService } from '../../services/google.service'; 3 | 4 | export const config: ApiRouteConfig = { 5 | type: 'api', 6 | name: 'Gmail Token Status', 7 | description: 'Checks the status of the Gmail token', 8 | path: '/api/token-status', 9 | method: 'GET', 10 | emits: ['gmail.token.status'], 11 | flows: ['gmail-flow'], 12 | } 13 | 14 | export const handler: StepHandler = async (req, {logger, state}) => { 15 | const googleService = new GoogleService(logger, state) 16 | 17 | try { 18 | const tokens = await googleService.getTokens() 19 | const expiryDate = tokens?.expiry_date ? new Date(tokens.expiry_date) : null 20 | const isExpired = expiryDate ? expiryDate < new Date() : false 21 | 22 | return { 23 | status: 200, 24 | body: { 25 | message: 'Successfully got tokens', 26 | expiryDate: tokens?.expiry_date, 27 | isExpired 28 | } 29 | } 30 | } catch (error) { 31 | logger.error(`Error getting tokens: ${error}`) 32 | return { 33 | status: 500, 34 | body: { 35 | message: 'Error getting tokens' 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /examples/gmail-workflow/steps/api/gmail-watch.step.ts: -------------------------------------------------------------------------------- 1 | import {ApiRouteConfig, StepHandler} from 'motia'; 2 | import { GoogleService } from '../../services/google.service'; 3 | 4 | export const config: ApiRouteConfig = { 5 | type: 'api', 6 | name: 'Gmail Watch', 7 | description: 'Watches Gmail for new emails', 8 | path: '/api/watch', 9 | method: 'GET', 10 | emits: ['gmail.watch'], 11 | flows: ['gmail-flow'], 12 | } 13 | 14 | export const handler: StepHandler = async (req, {logger, state}) => { 15 | const googleService = new GoogleService(logger, state) 16 | 17 | try { 18 | await googleService.watchEmail() 19 | } catch (error) { 20 | logger.error(`Error watching emails: ${error instanceof Error ? error.message : 'Unknown error'}`) 21 | return { 22 | status: 500, 23 | body: { 24 | message: 'Error watching emails' 25 | } 26 | } 27 | } 28 | 29 | return { 30 | status: 200, 31 | body: { 32 | message: 'Successfully watched emails' 33 | }, 34 | } 35 | } -------------------------------------------------------------------------------- /examples/gmail-workflow/steps/daily-summary.step.ts: -------------------------------------------------------------------------------- 1 | import { CronConfig, StepHandler } from 'motia'; 2 | import { DiscordService } from "../services/discord.service"; 3 | 4 | export const config: CronConfig = { 5 | type: 'cron', 6 | name: 'Daily Email Summary', 7 | description: 'Generates and sends a daily summary of processed emails to Discord', 8 | cron: '* * 1 * *', // every day at 1am 9 | emits: ['gmail.summary.sent'], 10 | flows: ['gmail-flow'] 11 | }; 12 | 13 | 14 | export const handler: StepHandler = async ({emit, logger, state}) => { 15 | logger.info('Generating daily email summary'); 16 | 17 | try { 18 | const discordService = new DiscordService(logger, state); 19 | const summary = await discordService.send(); 20 | 21 | await state.set('email_analysis', 'auto_responded_emails', []); 22 | await state.set('email_analysis', 'processed_emails', []); 23 | 24 | await emit({ 25 | topic: 'gmail.summary.sent', 26 | data: { 27 | date: (new Date()).toISOString().split('T')[0], 28 | summary, 29 | sentToDiscord: true 30 | } 31 | }); 32 | 33 | logger.info('Daily summary completed', {summary}); 34 | } catch (error) { 35 | logger.error(`Error generating daily summary: ${error instanceof Error ? error.message : String(error)}`); 36 | } 37 | }; -------------------------------------------------------------------------------- /examples/gmail-workflow/steps/fetch-email.step.ts: -------------------------------------------------------------------------------- 1 | import {EventConfig, StepHandler} from 'motia' 2 | import { EmailResponse, GoogleService } from '../services/google.service' 3 | import {z} from 'zod' 4 | 5 | const schema = z.object({ 6 | messageId: z.string(), 7 | historyId: z.number(), 8 | }) 9 | 10 | export const config: EventConfig = { 11 | type: 'event', 12 | name: 'Email Fetcher', 13 | description: 'Fetches email content from Gmail when triggered by an email received event', 14 | subscribes: ['gmail.email.received'], 15 | emits: [{ 16 | topic: 'gmail.email.fetched', 17 | label: 'Email Fetched', 18 | }], 19 | input: schema, 20 | flows: ['gmail-flow'], 21 | } 22 | 23 | export const handler: StepHandler = async (input, {emit, logger, state}) => { 24 | try { 25 | const payload = schema.parse(input) 26 | 27 | const {messageId, historyId} = payload 28 | const googleService = new GoogleService(logger, state); 29 | const data: EmailResponse = await googleService.getEmail(historyId.toString()) 30 | 31 | logger.info(`Emitting fetched email: ${JSON.stringify(data.subject)}`) 32 | 33 | await emit({ 34 | topic: 'gmail.email.fetched', 35 | data 36 | }) 37 | 38 | logger.info(`Email fetch completed successfully: ${messageId}`) 39 | } catch (error) { 40 | logger.error(`Error fetching email content ${error instanceof Error ? error.message : 'Unknown error'}`) 41 | } 42 | } -------------------------------------------------------------------------------- /examples/gmail-workflow/steps/gmail-webhook.step.ts: -------------------------------------------------------------------------------- 1 | import { ApiRouteConfig, StepHandler } from 'motia'; 2 | import { z } from 'zod'; 3 | 4 | const schema = z.object({ 5 | message: z.object({ 6 | data: z.string(), 7 | messageId: z.string(), 8 | }), 9 | }) 10 | 11 | type MessageData = { 12 | emailAddress: string 13 | historyId: number 14 | } 15 | 16 | 17 | export const config: ApiRouteConfig = { 18 | type: 'api', 19 | name: 'Webhook API', 20 | description: 'Receives webhook notifications from Gmail for new emails', 21 | path: '/api/gmail-webhook', 22 | method: 'POST', 23 | emits: [{ 24 | topic: 'gmail.email.received', 25 | label: 'Email Received', 26 | }], 27 | virtualSubscribes: ['api.gmail.webhook'], 28 | bodySchema: schema, 29 | flows: ['gmail-flow'], 30 | } 31 | 32 | export const handler: StepHandler = async (req, {logger, emit}) => { 33 | const payload = schema.parse(req.body) 34 | 35 | const messageData = Buffer.from(payload.message.data, 'base64').toString('utf-8') 36 | const message = JSON.parse(messageData) as MessageData 37 | 38 | logger.info(`Received email notification: ${JSON.stringify(message)}`) 39 | logger.info(`Received email notification: ${JSON.stringify(payload)}`) 40 | 41 | await emit({ 42 | topic: 'gmail.email.received', 43 | data: {messageId: payload.message.messageId, historyId: message.historyId} 44 | }) 45 | 46 | return { 47 | status: 200, 48 | body: { 49 | message: 'Email notification received and processing initiated' 50 | }, 51 | } 52 | } -------------------------------------------------------------------------------- /examples/gmail-workflow/steps/noops/gmail-webhook-simulator.step.ts: -------------------------------------------------------------------------------- 1 | import { NoopConfig } from 'motia' 2 | 3 | export const config: NoopConfig = { 4 | type: 'noop', 5 | name: 'Gmail Webhook Simulator', 6 | description: 'This node is used to simulate a Gmail webhook.', 7 | virtualEmits: [{ 8 | topic: 'api.gmail.webhook', 9 | label: 'Simulated Gmail Webhook', 10 | }], 11 | virtualSubscribes: [], 12 | flows: ['gmail-flow'], 13 | } 14 | -------------------------------------------------------------------------------- /examples/gmail-workflow/steps/noops/google-auth-noops.step.ts: -------------------------------------------------------------------------------- 1 | import {NoopConfig} from 'motia' 2 | 3 | export const config: NoopConfig = { 4 | type: 'noop', 5 | name: 'Google Auth', 6 | description: 'Fetches tokens from Google', 7 | virtualSubscribes: ['gmail.auth', 'gmail.auth-url', 'gmail.watch', 'gmail.token.status'], 8 | virtualEmits: ['gmail.auth'], 9 | flows: ['gmail-flow'], 10 | } 11 | -------------------------------------------------------------------------------- /examples/gmail-workflow/steps/noops/human-review.step.ts: -------------------------------------------------------------------------------- 1 | import {NoopConfig} from 'motia' 2 | 3 | export const config: NoopConfig = { 4 | type: 'noop', 5 | name: 'Human Email Review', 6 | description: 'Manual review of emails flagged as suspicious or requiring human attention', 7 | virtualSubscribes: ['gmail.email.analyzed'], 8 | virtualEmits: ['gmail.email.reviewed'], 9 | flows: ['gmail-flow'], 10 | } -------------------------------------------------------------------------------- /examples/gmail-workflow/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "resolveJsonModule": true, 11 | "allowJs": true, 12 | "outDir": "dist", 13 | "rootDir": ".", 14 | "baseUrl": ".", 15 | "jsx": "react-jsx", 16 | "types": ["jest", "node"] 17 | }, 18 | "include": [ 19 | "**/*.ts", 20 | "**/*.tsx", 21 | "**/*.js", 22 | "**/*.jsx" 23 | ], 24 | "exclude": [ 25 | "node_modules", 26 | "dist" 27 | ] 28 | } -------------------------------------------------------------------------------- /examples/motia-parallel-execution/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | python_modules 3 | .venv 4 | venv 5 | .motia 6 | .mermaid 7 | dist 8 | *.pyc 9 | package-lock.json -------------------------------------------------------------------------------- /examples/motia-parallel-execution/docs/images/motia-parallel-exec.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MotiaDev/motia-examples/c1ea1854d55a9d75218e6c00a8302dcb98289bb8/examples/motia-parallel-execution/docs/images/motia-parallel-exec.gif -------------------------------------------------------------------------------- /examples/motia-parallel-execution/motia-workbench.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": { 3 | "steps/03-check-state-change.step.ts": { 4 | "x": 20, 5 | "y": 634 6 | }, 7 | "steps/02-test-state.step.ts": { 8 | "x": 4.818698394670406, 9 | "y": 388.0566456408603 10 | }, 11 | "steps/01-api.step.ts": { 12 | "x": 17.5, 13 | "y": 188 14 | }, 15 | "steps/00-noop.step.ts": { 16 | "x": 56, 17 | "y": 0 18 | } 19 | }, 20 | "parallel-processing": { 21 | "steps/word-count.step.ts": { 22 | "x": 117.75, 23 | "y": 237 24 | }, 25 | "steps/sentiment-analysis.step.ts": { 26 | "x": 451.7726781857451, 27 | "y": 231.4114470842332 28 | }, 29 | "steps/noop.step.ts": { 30 | "x": 387.18592219939006, 31 | "y": -207.1875529841634 32 | }, 33 | "steps/keyword-extraction.step.ts": { 34 | "x": 808.1495680345572, 35 | "y": 237 36 | }, 37 | "steps/data-processing-api.step.ts": { 38 | "x": 410, 39 | "y": 0 40 | }, 41 | "steps/data-aggregator.step.ts": { 42 | "x": 409.3153347732182, 43 | "y": 444.75809935205183 44 | }, 45 | "steps/08-data-aggregator.step.ts": { 46 | "x": 1445, 47 | "y": 446 48 | }, 49 | "steps/07-keyword-extraction.step.ts": { 50 | "x": 911, 51 | "y": 237 52 | }, 53 | "steps/06-sentiment-analysis.step.ts": { 54 | "x": 1183, 55 | "y": 237 56 | }, 57 | "steps/05-word-count.step.ts": { 58 | "x": 1513.5, 59 | "y": 237 60 | }, 61 | "steps/04-data-processing-api.step.ts": { 62 | "x": 907, 63 | "y": 0 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /examples/motia-parallel-execution/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-agent", 3 | "description": "", 4 | "scripts": { 5 | "dev": "motia dev --verbose", 6 | "dev:debug": "motia dev --debug", 7 | "generate-types": "motia generate-types", 8 | "build": "motia build", 9 | "clean": "rm -rf dist node_modules python_modules .motia .mermaid" 10 | }, 11 | "keywords": [ 12 | "motia" 13 | ], 14 | "dependencies": { 15 | "motia": "^0.2.1-beta.73", 16 | "zod": "^3.25.49" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^18.3.23", 20 | "ts-node": "^10.9.2", 21 | "typescript": "^5.8.3" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/motia-parallel-execution/steps/data-processing-api.step.ts: -------------------------------------------------------------------------------- 1 | import { ApiRouteConfig, Handlers } from 'motia' 2 | import { z } from 'zod' 3 | 4 | export const config: ApiRouteConfig = { 5 | type: 'api', 6 | name: 'DataProcessingApi', 7 | description: 'Trigger parallel data processing workflow', 8 | method: 'POST', 9 | path: '/process-data', 10 | emits: ['parallel-processing'], 11 | bodySchema: z.object({ 12 | text: z.string().min(1, 'Text content is required'), 13 | id: z.string().optional() 14 | }), 15 | responseSchema: { 16 | 200: z.object({ 17 | message: z.string(), 18 | traceId: z.string(), 19 | streamId: z.string() 20 | }) 21 | }, 22 | flows: ['parallel-processing'], 23 | } 24 | 25 | export const handler: Handlers['DataProcessingApi'] = async (req, { traceId, logger, emit, streams }) => { 26 | logger.info('Step 04 – Starting parallel data processing', { body: req.body }) 27 | 28 | const processingId = req.body.id || `processing-${Date.now()}` 29 | 30 | try { 31 | await streams.processingProgress.set(traceId, processingId, { 32 | status: 'started', 33 | progress: 0, 34 | results: {} 35 | }) 36 | } catch (error) { 37 | logger.warn('Stream not available, continuing without progress tracking', { error }) 38 | } 39 | 40 | await emit({ 41 | topic: 'parallel-processing', 42 | data: { 43 | text: req.body.text, 44 | processingId 45 | }, 46 | }) 47 | 48 | return { 49 | status: 200, 50 | body: { 51 | message: 'Parallel processing started', 52 | traceId, 53 | streamId: processingId 54 | }, 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /examples/motia-parallel-execution/steps/keyword-extraction.step.ts: -------------------------------------------------------------------------------- 1 | import { EventConfig, Handlers } from 'motia' 2 | import { z } from 'zod' 3 | 4 | export const config: EventConfig = { 5 | type: 'event', 6 | name: 'KeywordExtractor', 7 | description: 'Extract keywords in parallel', 8 | subscribes: ['parallel-processing'], 9 | emits: ['processing-result'], 10 | input: z.object({ 11 | text: z.string(), 12 | processingId: z.string() 13 | }), 14 | flows: ['parallel-processing'], 15 | } 16 | 17 | export const handler: Handlers['KeywordExtractor'] = async (input, { traceId, logger, emit, streams }) => { 18 | logger.info('Step 07 – Processing keyword extraction', { processingId: input.processingId }) 19 | 20 | await new Promise(resolve => setTimeout(resolve, 800)) 21 | 22 | const stopWords = ['the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', 'is', 'are', 'was', 'were', 'be', 'been', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', 'should'] 23 | 24 | const words = input.text.toLowerCase() 25 | .replace(/[^\w\s]/g, '') 26 | .split(/\s+/) 27 | .filter((word: string) => word.length > 3 && !stopWords.includes(word)) 28 | 29 | const wordFreq = words.reduce((acc: Record, word: string) => { 30 | acc[word] = (acc[word] || 0) + 1 31 | return acc 32 | }, {} as Record) 33 | 34 | const keywords = Object.entries(wordFreq) 35 | .sort(([,a], [,b]) => (b as number) - (a as number)) 36 | .slice(0, 5) 37 | .map(([word]) => word) 38 | 39 | await emit({ 40 | topic: 'processing-result', 41 | data: { 42 | type: 'keywords', 43 | result: keywords, 44 | processingId: input.processingId 45 | } 46 | }) 47 | 48 | logger.info('Step 07 – Keyword extraction completed', { keywords, processingId: input.processingId }) 49 | } 50 | -------------------------------------------------------------------------------- /examples/motia-parallel-execution/steps/processing-progress.stream.ts: -------------------------------------------------------------------------------- 1 | import { StateStreamConfig } from 'motia' 2 | import { z } from 'zod' 3 | 4 | export const config: StateStreamConfig = { 5 | name: 'processingProgress', 6 | schema: z.object({ 7 | status: z.enum(['started', 'processing', 'completed', 'error']), 8 | progress: z.number().min(0).max(100), 9 | results: z.object({ 10 | wordCount: z.number().optional(), 11 | sentiment: z.string().optional(), 12 | keywords: z.array(z.string()).optional(), 13 | summary: z.string().optional() 14 | }) 15 | }), 16 | baseConfig: { 17 | storageType: 'state', 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /examples/motia-parallel-execution/steps/sentiment-analysis.step.ts: -------------------------------------------------------------------------------- 1 | import { EventConfig, Handlers } from 'motia' 2 | import { z } from 'zod' 3 | 4 | export const config: EventConfig = { 5 | type: 'event', 6 | name: 'SentimentAnalyzer', 7 | description: 'Analyze sentiment in parallel', 8 | subscribes: ['parallel-processing'], 9 | emits: ['processing-result'], 10 | input: z.object({ 11 | text: z.string(), 12 | processingId: z.string() 13 | }), 14 | flows: ['parallel-processing'], 15 | } 16 | 17 | export const handler: Handlers['SentimentAnalyzer'] = async (input, { traceId, logger, emit, streams }) => { 18 | logger.info('Step 06 – Processing sentiment analysis', { processingId: input.processingId }) 19 | 20 | await new Promise(resolve => setTimeout(resolve, 1500)) 21 | 22 | const positiveWords = ['good', 'great', 'excellent', 'amazing', 'wonderful', 'fantastic', 'love', 'like'] 23 | const negativeWords = ['bad', 'terrible', 'awful', 'hate', 'dislike', 'horrible', 'worst'] 24 | 25 | const text = input.text.toLowerCase() 26 | const positiveCount = positiveWords.filter(word => text.includes(word)).length 27 | const negativeCount = negativeWords.filter(word => text.includes(word)).length 28 | 29 | let sentiment = 'neutral' 30 | if (positiveCount > negativeCount) sentiment = 'positive' 31 | else if (negativeCount > positiveCount) sentiment = 'negative' 32 | 33 | await emit({ 34 | topic: 'processing-result', 35 | data: { 36 | type: 'sentiment', 37 | result: sentiment, 38 | processingId: input.processingId 39 | } 40 | }) 41 | 42 | logger.info('Step 06 – Sentiment analysis completed', { sentiment, processingId: input.processingId }) 43 | } 44 | -------------------------------------------------------------------------------- /examples/motia-parallel-execution/steps/word-count.step.ts: -------------------------------------------------------------------------------- 1 | import { EventConfig, Handlers } from 'motia' 2 | import { z } from 'zod' 3 | 4 | export const config: EventConfig = { 5 | type: 'event', 6 | name: 'WordCountProcessor', 7 | description: 'Count words in parallel', 8 | subscribes: ['parallel-processing'], 9 | emits: ['processing-result'], 10 | input: z.object({ 11 | text: z.string(), 12 | processingId: z.string() 13 | }), 14 | flows: ['parallel-processing'], 15 | } 16 | 17 | export const handler: Handlers['WordCountProcessor'] = async (input, { traceId, logger, emit, streams }) => { 18 | logger.info('Step 05 – Processing word count', { processingId: input.processingId }) 19 | 20 | await new Promise(resolve => setTimeout(resolve, 1000)) 21 | 22 | const wordCount = input.text.split(/\s+/).filter(word => word.length > 0).length 23 | 24 | await emit({ 25 | topic: 'processing-result', 26 | data: { 27 | type: 'wordCount', 28 | result: wordCount, 29 | processingId: input.processingId 30 | } 31 | }) 32 | 33 | logger.info('Step 05 – Word count completed', { wordCount, processingId: input.processingId }) 34 | } 35 | -------------------------------------------------------------------------------- /examples/motia-parallel-execution/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "resolveJsonModule": true, 11 | "allowJs": true, 12 | "outDir": "dist", 13 | "rootDir": ".", 14 | "baseUrl": ".", 15 | "jsx": "react-jsx" 16 | }, 17 | "include": [ 18 | "**/*.ts", 19 | "**/*.tsx", 20 | "**/*.js", 21 | "**/*.jsx", 22 | "types.d.ts" 23 | ], 24 | "exclude": [ 25 | "node_modules", 26 | "dist", 27 | "tests" 28 | ] 29 | } -------------------------------------------------------------------------------- /examples/motia-parallel-execution/types.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Automatically generated types for motia 3 | * Do NOT edit this file manually. 4 | * 5 | * Consider adding this file to .prettierignore and eslint ignore. 6 | */ 7 | import { EventHandler, ApiRouteHandler, ApiResponse, IStateStream } from 'motia' 8 | 9 | declare module 'motia' { 10 | interface FlowContextStateStreams { 11 | 'processingProgress': IStateStream<{ status: string; progress: number; results: { wordCount?: number; sentiment?: string; keywords?: string[]; summary?: string } }> 12 | } 13 | 14 | type Handlers = { 15 | 'WordCountProcessor': EventHandler<{ text: string; processingId: string }, { topic: 'processing-result'; data: { type: string; result: unknown; processingId: string } }> 16 | 'SentimentAnalyzer': EventHandler<{ text: string; processingId: string }, { topic: 'processing-result'; data: { type: string; result: unknown; processingId: string } }> 17 | 'KeywordExtractor': EventHandler<{ text: string; processingId: string }, { topic: 'processing-result'; data: { type: string; result: unknown; processingId: string } }> 18 | 'DataProcessingApi': ApiRouteHandler<{ text: string; id?: string }, ApiResponse<200, { message: string; traceId: string; streamId: string }>, { topic: 'parallel-processing'; data: { text: string; processingId: string } }> 19 | 'DataAggregator': EventHandler<{ type: string; result: unknown; processingId: string }, never> 20 | } 21 | } -------------------------------------------------------------------------------- /examples/rag-docling-weaviate-agent/.env.example: -------------------------------------------------------------------------------- 1 | # Weaviate Configuration 2 | WEAVIATE_URL=http://localhost:8080 3 | WEAVIATE_API_KEY=your-weaviate-api-key 4 | 5 | # OpenAI Configuration 6 | OPENAI_API_KEY=your-openai-api-key 7 | -------------------------------------------------------------------------------- /examples/rag-docling-weaviate-agent/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .motia 3 | dist 4 | build 5 | coverage 6 | pnpm-lock.yaml 7 | .DS_Store 8 | data/ 9 | .venv/ 10 | .mermaid/ 11 | .cursor/ -------------------------------------------------------------------------------- /examples/rag-docling-weaviate-agent/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "es5", 4 | "singleQuote": true, 5 | "printWidth": 100, 6 | "tabWidth": 2, 7 | "endOfLine": "auto" 8 | } -------------------------------------------------------------------------------- /examples/rag-docling-weaviate-agent/docs/images/workbench.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MotiaDev/motia-examples/c1ea1854d55a9d75218e6c00a8302dcb98289bb8/examples/rag-docling-weaviate-agent/docs/images/workbench.png -------------------------------------------------------------------------------- /examples/rag-docling-weaviate-agent/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "eslint/config"; 2 | import eslint from '@eslint/js'; 3 | import tseslint from 'typescript-eslint'; 4 | import tsParser from '@typescript-eslint/parser'; 5 | import eslintPluginPrettier from 'eslint-plugin-prettier'; 6 | import globals from 'globals'; 7 | 8 | const ignoreConfig = { 9 | ignores: ['**/node_modules/**', '**/.motia/**', '**/dist/**', '**/build/**', '**/coverage/**', '**/.venv/**'] 10 | }; 11 | 12 | const typescriptConfig = { 13 | files: ['**/*.ts', '**/*.tsx'], 14 | languageOptions: { 15 | parser: tsParser, 16 | parserOptions: { 17 | project: './tsconfig.json' 18 | }, 19 | globals: { 20 | ...globals.node 21 | } 22 | }, 23 | plugins: { 24 | 'prettier': eslintPluginPrettier 25 | }, 26 | rules: { 27 | // Prettier 28 | 'prettier/prettier': 'warn', 29 | 30 | // General 31 | 'no-console': ['warn', { allow: ['warn', 'error'] }], 32 | 'eqeqeq': 'error', 33 | 'no-duplicate-imports': 'error', 34 | 'no-unused-expressions': 'error', 35 | 'no-unused-vars': ['error', { argsIgnorePattern: '^_' }], 36 | 'curly': 'error' 37 | }, 38 | }; 39 | 40 | export default defineConfig([ 41 | ignoreConfig, 42 | eslint.configs.recommended, 43 | tseslint.configs.recommended, 44 | typescriptConfig, 45 | ]); -------------------------------------------------------------------------------- /examples/rag-docling-weaviate-agent/motia-workbench.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": {}, 3 | "resume-analysis": { 4 | "steps/api-steps/trigger-resume-analysis.step.ts": { 5 | "x": 0, 6 | "y": 0 7 | }, 8 | "steps/event-steps/analyze-resume.step.ts": { 9 | "x": 26.5, 10 | "y": 204 11 | }, 12 | "steps/event-steps/match-positions.step.ts": { 13 | "x": 24.5, 14 | "y": 380 15 | }, 16 | "steps/event-steps/generate-summary.step.ts": { 17 | "x": 16, 18 | "y": 556 19 | } 20 | }, 21 | "rag-workflow": { 22 | "steps/api-steps/process-pdfs.step.ts": { 23 | "x": 0, 24 | "y": 0 25 | }, 26 | "steps/api-steps/query-rag.step.ts": { 27 | "x": 655.2918192098077, 28 | "y": 2.4081930274456838 29 | }, 30 | "steps/event-steps/load-weaviate.step.ts": { 31 | "x": 305.0937068114117, 32 | "y": 493.58856459595967 33 | }, 34 | "steps/event-steps/process-pdfs.step.py": { 35 | "x": -98.46956788260296, 36 | "y": 480.82815788552796 37 | }, 38 | "steps/api-steps/trigger-pdf-processing.step.ts": { 39 | "x": -96.56887191558423, 40 | "y": 3.2109240365942426 41 | }, 42 | "steps/event-steps/process-pdf.step.py": { 43 | "x": 326.8149917789989, 44 | "y": 192.3713494507399 45 | }, 46 | "steps/event-steps/read-pdfs.step.ts": { 47 | "x": -85.02320890063677, 48 | "y": 205.36836385957574 49 | }, 50 | "steps/api-steps/api-process-pdfs.step.ts": { 51 | "x": 71.25, 52 | "y": 0 53 | }, 54 | "steps/api-steps/api-query-rag.step.ts": { 55 | "x": 622.5285180150489, 56 | "y": 2.064770527296588 57 | }, 58 | "steps/event-steps/init-weaviate.step.ts": { 59 | "x": 297.3269559307097, 60 | "y": 203.99999999999997 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /examples/rag-docling-weaviate-agent/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rag-docling-weaviate-agent", 3 | "version": "1.0.0", 4 | "description": "RAG Agent to process and vectorize PDF documents using Motia, Docling and Weaviate", 5 | "main": "index.js", 6 | "scripts": { 7 | "prepare": "python3 -m venv python_modules && source python_modules/bin/activate && pip install -r requirements.txt", 8 | "dev": "source python_modules/bin/activate && motia dev", 9 | "dev:debug": "source python_modules/bin/activate && motia dev --debug", 10 | "build": "source python_modules/bin/activate && motia build", 11 | "clean": "rm -rf dist .motia .mermaid node_modules python_modules", 12 | "generate:config": "motia get-config --output ./", 13 | "lint": "eslint . --ext .ts,.tsx", 14 | "lint:fix": "eslint . --ext .ts,.tsx --fix", 15 | "format": "prettier --write \"**/*.{ts,tsx,json,md}\"", 16 | "format:check": "prettier --check \"**/*.{ts,tsx,json,md}\"", 17 | "test": "jest --coverage" 18 | }, 19 | "keywords": [ 20 | "motia", 21 | "rag", 22 | "pdf", 23 | "weaviate" 24 | ], 25 | "author": "Filipe Leandro ", 26 | "license": "MIT", 27 | "dependencies": { 28 | "motia": "0.1.0-beta.25", 29 | "openai": "^4.91.0", 30 | "weaviate-client": "^3.4.2", 31 | "zod": "^3.24.2" 32 | }, 33 | "devDependencies": { 34 | "@eslint/js": "^9.23.0", 35 | "@types/jest": "^29.5.14", 36 | "@types/node": "^20.17.28", 37 | "@typescript-eslint/parser": "^8.29.0", 38 | "eslint": "^9.23.0", 39 | "eslint-config-prettier": "^10.1.1", 40 | "eslint-plugin-prettier": "^5.2.5", 41 | "globals": "^16.0.0", 42 | "jest": "^29.7.0", 43 | "prettier": "^3.5.3", 44 | "typescript": "^5.8.2", 45 | "typescript-eslint": "^8.29.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /examples/rag-docling-weaviate-agent/requirements.txt: -------------------------------------------------------------------------------- 1 | docling>=2.7.0 2 | transformers>=4.50.3 -------------------------------------------------------------------------------- /examples/rag-docling-weaviate-agent/steps/api-steps/api-process-pdfs.step.ts: -------------------------------------------------------------------------------- 1 | import { ApiRequest, ApiRouteConfig, FlowContext, StepHandler } from 'motia'; 2 | import { z } from 'zod'; 3 | 4 | export const config: ApiRouteConfig = { 5 | type: 'api', 6 | name: 'api-process-pdfs', 7 | path: '/api/rag/process-pdfs', 8 | method: 'POST', 9 | emits: [{ topic: 'rag.read.pdfs' }], 10 | flows: ['rag-workflow'], 11 | bodySchema: z.object({ 12 | folderPath: z.string(), 13 | }), 14 | }; 15 | 16 | export const handler: StepHandler = async ( 17 | req: ApiRequest, 18 | { emit, logger }: FlowContext 19 | ) => { 20 | const { folderPath } = req.body; 21 | 22 | logger.info('Starting PDF processing workflow', { folderPath }); 23 | 24 | await emit({ 25 | topic: 'rag.read.pdfs', 26 | data: { folderPath }, 27 | }); 28 | 29 | return { 30 | status: 200, 31 | body: { 32 | message: 'PDF processing workflow started', 33 | folderPath, 34 | }, 35 | }; 36 | }; 37 | -------------------------------------------------------------------------------- /examples/rag-docling-weaviate-agent/steps/event-steps/read-pdfs.step.ts: -------------------------------------------------------------------------------- 1 | import { readdir } from 'fs/promises'; 2 | import { join } from 'path'; 3 | import { EventConfig, FlowContext, StepHandler } from 'motia'; 4 | import { z } from 'zod'; 5 | 6 | const InputSchema = z.object({ 7 | folderPath: z.string(), 8 | }); 9 | 10 | export const config: EventConfig = { 11 | type: 'event', 12 | name: 'read-pdfs', 13 | flows: ['rag-workflow'], 14 | subscribes: ['rag.read.pdfs'], 15 | emits: [{ topic: 'rag.process.pdfs', label: 'Start processing PDFs' }], 16 | input: InputSchema, 17 | }; 18 | 19 | export const handler: StepHandler = async ( 20 | input: z.infer, 21 | { emit, logger }: FlowContext 22 | ) => { 23 | const { folderPath } = input; 24 | logger.info(`Reading PDFs from folder: ${folderPath}`); 25 | 26 | // Read all files in the directory 27 | const files = await readdir(folderPath); 28 | const pdfFiles = files.filter((file) => file.endsWith('.pdf')); 29 | 30 | logger.info(`Found ${pdfFiles.length} PDF files`); 31 | 32 | const filesInfo = await Promise.all( 33 | pdfFiles.map(async (pdfFile) => { 34 | const filePath = join(folderPath, pdfFile); 35 | return { 36 | filePath, 37 | fileName: pdfFile, 38 | }; 39 | }) 40 | ); 41 | 42 | // Process PDF files in parallel 43 | /*await Promise.all( 44 | filesInfo.map(async (file) => { 45 | await emit({ 46 | topic: 'rag.process.pdf', 47 | data: { files: [file] }, 48 | }); 49 | }) 50 | );*/ 51 | 52 | // Process PDF files sequentially 53 | await emit({ 54 | topic: 'rag.process.pdfs', 55 | data: { files: filesInfo }, 56 | }); 57 | }; 58 | -------------------------------------------------------------------------------- /examples/rag-docling-weaviate-agent/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ESNext", 5 | "lib": ["ES2020"], 6 | "outDir": "./dist", 7 | "rootDir": "./", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "resolveJsonModule": true, 13 | "moduleResolution": "node", 14 | "declaration": true, 15 | "sourceMap": true 16 | }, 17 | "include": ["**/*.ts"], 18 | "exclude": ["node_modules", "dist", "**/*.test.ts"] 19 | } 20 | -------------------------------------------------------------------------------- /examples/rag-docling-weaviate-agent/types/index.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | // Input schema for processing PDFs 4 | export const ProcessPDFsInput = z.object({ 5 | folder_path: z.string(), 6 | }); 7 | 8 | // Input schema for querying the RAG system 9 | export const QueryRAGInput = z.object({ 10 | query: z.string(), 11 | limit: z.number().optional().default(3), 12 | }); 13 | 14 | // Schema for document chunks 15 | export const DocumentChunk = z.object({ 16 | text: z.string(), 17 | title: z.string(), 18 | metadata: z.object({ 19 | source: z.string(), 20 | page: z.number(), 21 | }), 22 | }); 23 | 24 | // Schema for RAG response 25 | export const RAGResponse = z.object({ 26 | query: z.string(), 27 | answer: z.string(), 28 | chunks: z.array(DocumentChunk), 29 | }); 30 | 31 | export type ProcessPDFsInputType = z.infer; 32 | export type QueryRAGInputType = z.infer; 33 | export type DocumentChunkType = z.infer; 34 | export type RAGResponseType = z.infer; 35 | -------------------------------------------------------------------------------- /examples/rag_example/.mermaid/parse-embed-rag.mmd: -------------------------------------------------------------------------------- 1 | flowchart TD 2 | classDef apiStyle fill:#f96,stroke:#333,stroke-width:2px,color:#fff 3 | classDef eventStyle fill:#69f,stroke:#333,stroke-width:2px,color:#fff 4 | classDef cronStyle fill:#9c6,stroke:#333,stroke-width:2px,color:#fff 5 | classDef noopStyle fill:#3f3a50,stroke:#333,stroke-width:2px,color:#fff 6 | steps_chunk_step_py["⚡ Chunk & Save"]:::eventStyle 7 | steps_embed_step_py["⚡ Embed & Save"]:::eventStyle 8 | steps_index_step_py["⚡ Index & Save"]:::eventStyle 9 | steps_parse_step_py["🌐 Parser API"]:::apiStyle 10 | steps_rag_api_step_py["🌐 Rag API"]:::apiStyle 11 | steps_chunk_step_py -->|chunk.complete| steps_embed_step_py 12 | steps_chunk_step_py -->|chunk.complete| steps_index_step_py 13 | steps_embed_step_py -->|embed.complete| steps_index_step_py 14 | steps_parse_step_py -->|parse.complete| steps_chunk_step_py 15 | -------------------------------------------------------------------------------- /examples/rag_example/docs/images/parse-embed-rag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MotiaDev/motia-examples/c1ea1854d55a9d75218e6c00a8302dcb98289bb8/examples/rag_example/docs/images/parse-embed-rag.png -------------------------------------------------------------------------------- /examples/rag_example/motia-workbench.json: -------------------------------------------------------------------------------- 1 | { 2 | "parse-embed-rag": { 3 | "steps/chunk.step.py": { 4 | "x": 90.5, 5 | "y": 256 6 | }, 7 | "steps/embed.step.py": { 8 | "x": 229.59075630252102, 9 | "y": 430.8168067226891 10 | }, 11 | "steps/index.step.py": { 12 | "x": 93.5, 13 | "y": 608 14 | }, 15 | "steps/parse.step.py": { 16 | "x": 0, 17 | "y": 0 18 | }, 19 | "steps/rag_api.step.py": { 20 | "x": 410, 21 | "y": 0 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /examples/rag_example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-motia-project", 3 | "description": "", 4 | "scripts": { 5 | "dev": "motia dev", 6 | "dev:debug": "motia dev --debug", 7 | "generate:config": "motia get-config --output ./" 8 | }, 9 | "keywords": [ 10 | "motia" 11 | ], 12 | "dependencies": { 13 | "motia": "^0.1.0-beta.15", 14 | "react": "^19.0.0", 15 | "zod": "^3.24.2" 16 | }, 17 | "devDependencies": { 18 | "@types/react": "^19.0.12", 19 | "ts-node": "^10.9.2", 20 | "typescript": "^5.8.2" 21 | } 22 | } -------------------------------------------------------------------------------- /examples/rag_example/rag_config.yml: -------------------------------------------------------------------------------- 1 | llm_model: "gemini-1.5-flash" 2 | embedding_model: "models/embedding-001" 3 | chunk_size: 256 4 | chunk_overlap: 32 5 | prompt: | 6 | You are an AI assistant tasked with answering a user's query based on retrieved information. 7 | Read the provided retrieved information to best answer the user's query. If the response is not sufficient, supplement the retrieved information with your knowledge. Then, summarize the provided retrieved information in the context of the query, making your answer sound like a natural, human response. Focus on clarity and conciseness. 8 | 9 | ### Query: 10 | {query} 11 | 12 | ### Retrieved information: 13 | {responses} 14 | num_retrival: 5 -------------------------------------------------------------------------------- /examples/rag_example/requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | beautifulsoup4 3 | google-generativeai 4 | faiss-cpu 5 | numpy 6 | pyyaml -------------------------------------------------------------------------------- /examples/rag_example/src/index.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pathlib 3 | import numpy as np 4 | import faiss 5 | from typing import Optional 6 | 7 | def read_embeddings() -> Optional[np.ndarray]: 8 | """ 9 | Reads embeddings from a file and returns them as a NumPy array. 10 | 11 | Returns: 12 | Optional[np.ndarray]: The embeddings as a NumPy array, or None if an error occurs. 13 | """ 14 | current_dir = pathlib.Path().parent.resolve() 15 | embeddings_dir = current_dir / "temp" / "embeddings" 16 | embeddings_dir.mkdir(parents=True, exist_ok=True) # Ensure the directory exists 17 | filepath = embeddings_dir / "embeddings_file.npy" 18 | 19 | try: 20 | arr = np.load(filepath) # Use np.load to read binary format 21 | print(f"ndarray successfully read from: {filepath}") 22 | return arr 23 | except FileNotFoundError: 24 | print(f"File not found: {filepath}") 25 | return None 26 | except Exception as e: 27 | print(f"Error reading ndarray from {filepath}: {e}") 28 | return None 29 | 30 | def index_embeddings() -> None: 31 | """ 32 | Reads embeddings, creates a Faiss index, and saves the index to a file. 33 | """ 34 | current_dir = pathlib.Path().parent.resolve() 35 | faiss_dir = current_dir / "temp" / "faiss_files" 36 | faiss_dir.mkdir(parents=True, exist_ok=True) # Ensure the directory exists 37 | filepath = faiss_dir / "vector_index.bin" 38 | 39 | embeddings = read_embeddings() 40 | if embeddings is None: 41 | print("No embeddings to index.") 42 | return 43 | 44 | n, dim = embeddings.shape 45 | index = faiss.IndexFlatL2(dim) 46 | index.add(embeddings) 47 | 48 | try: 49 | faiss.write_index(index, str(filepath)) 50 | print(f"Index saved to {filepath}") 51 | except Exception as e: 52 | print(f"Error saving index to {filepath}: {e}") 53 | 54 | if __name__ == "__main__": 55 | try: 56 | index_embeddings() 57 | except Exception as e: 58 | print(f"Indexing Failed: {e}") -------------------------------------------------------------------------------- /examples/rag_example/src/parse.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from bs4 import BeautifulSoup 3 | import argparse 4 | import json 5 | import pathlib 6 | from typing import Optional 7 | 8 | def fetch_confluence_page(page_url: str) -> str: 9 | """ 10 | Fetches and parses the content of a Confluence page. 11 | 12 | Args: 13 | page_url (str): The URL of the Confluence page. 14 | 15 | Returns: 16 | str: The parsed text content of the page. 17 | """ 18 | try: 19 | response = requests.get(page_url) 20 | response.raise_for_status() 21 | soup = BeautifulSoup(response.text, "html.parser") 22 | body = soup.find("body") 23 | if not body: 24 | return "" 25 | text = body.get_text("\n") 26 | text = " ".join(text.split()) # Normalize whitespace 27 | print("Website parsed successfully.") 28 | return text 29 | except requests.exceptions.RequestException as e: 30 | print(f"Error fetching URL: {e}") 31 | return "" 32 | 33 | def save_text_to_file(url: str) -> None: 34 | """ 35 | Fetches the content of a Confluence page and saves it to a file. 36 | 37 | Args: 38 | url (str): The URL of the Confluence page. 39 | """ 40 | text = fetch_confluence_page(url) 41 | if not text: 42 | print("No text to save.") 43 | return 44 | 45 | current_dir = pathlib.Path().parent.resolve() 46 | text_dir = current_dir / "temp" / "text" 47 | text_dir.mkdir(parents=True, exist_ok=True) # Ensure the directory exists 48 | filepath = text_dir / "parsed.txt" 49 | 50 | try: 51 | with open(filepath, "w", encoding="utf-8") as f: # Explicit encoding 52 | f.write(text) 53 | print(f"Text saved to {filepath}") 54 | except Exception as e: 55 | print(f"Error saving text to file: {e}") 56 | 57 | if __name__ == "__main__": 58 | parser = argparse.ArgumentParser(description="Fetch and chunk a Confluence page.") 59 | parser.add_argument("url", help="URL of the Confluence page") 60 | args = parser.parse_args() 61 | 62 | save_text_to_file(args.url) 63 | -------------------------------------------------------------------------------- /examples/rag_example/steps/chunk.step.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pathlib 3 | import subprocess 4 | import logging 5 | 6 | config = { 7 | 'type': 'event', 8 | 'name': 'Chunk & Save', 9 | 'description': 'Reads raw text from temp/text, chunks and saves to temp/chunks', 10 | 'subscribes': ['parse.complete'], 11 | 'emits': ['chunk.complete'], 12 | 'flows': ['parse-embed-rag'], 13 | } 14 | 15 | async def handler(req, ctx): 16 | """ 17 | Handler function to run the chunking script and emit the completion event. 18 | 19 | Args: 20 | req: The request object. 21 | ctx: The context object. 22 | """ 23 | script_path = pathlib.Path().parent.parent / "src" / "chunk.py" 24 | 25 | try: 26 | result = subprocess.run(["python3", str(script_path)], capture_output=True, text=True) 27 | if result.returncode == 0: 28 | ctx.logger.info("Parsed website text was read, chunked, and saved") 29 | else: 30 | ctx.logger.error(f"Error chunking parsed website data: {result.stderr}") 31 | except Exception as e: 32 | ctx.logger.error(f"Error chunking parsed website data: {e}") 33 | 34 | await ctx.emit({ 35 | 'topic': 'chunk.complete', 36 | 'data': {'message': 'chunking completed'} 37 | }) 38 | return -------------------------------------------------------------------------------- /examples/rag_example/steps/embed.step.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import subprocess 3 | import os 4 | 5 | config = { 6 | 'type': 'event', 7 | 'name': 'Embed & Save', 8 | 'description': 'Reads chunked data, embeds and saves it to temp/embeddings', 9 | 'subscribes': ['chunk.complete'], 10 | 'emits': ['embed.complete'], 11 | 'flows': ['parse-embed-rag'], 12 | } 13 | 14 | async def handler(req, ctx): 15 | script_path = pathlib.Path().parent / "src" / "embed.py" 16 | try: 17 | subprocess.run(["python", str(script_path)]) 18 | ctx.logger.info("Chunked Text was embedded successfully!") 19 | except Exception as e: 20 | ctx.logger.info(f"Error embedding chunked text: {e}") 21 | 22 | await ctx.emit({ 23 | 'topic': 'embed.complete', 24 | 'data': {'message': 'embedding completed'} 25 | }) 26 | return -------------------------------------------------------------------------------- /examples/rag_example/steps/index.step.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pathlib 3 | import subprocess 4 | 5 | config = { 6 | 'type': 'event', 7 | 'name': 'Index & Save', 8 | 'description': 'Reads Embedding files from temp/embeddings, indexes them in Faiss and saves the index to temp/faiss_files', 9 | 'subscribes': ['chunk.complete', 'embed.complete'], 10 | 'emits': ['index.complete'], 11 | 'flows': ['parse-embed-rag'], 12 | } 13 | 14 | async def handler(req, ctx): 15 | script_path = pathlib.Path().parent / "src" / "index.py" 16 | 17 | try: 18 | subprocess.run(["python", str(script_path)]) 19 | ctx.logger.info("Embedded data was indexed successfully!") 20 | except Exception as e: 21 | ctx.logger.info(f"Error indexing embedded text: {e}") 22 | 23 | await ctx.emit({ 24 | 'topic': 'index.complete', 25 | 'data': {'message': 'indexing completed'} 26 | }) 27 | return -------------------------------------------------------------------------------- /examples/rag_example/steps/parse.step.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import subprocess 3 | 4 | config = { 5 | 'type': 'api', 6 | 'name': 'Parser API', 7 | 'description': 'Parses and chunks a given url and saves it to temp/chunks', 8 | 'path': '/api/parse', 9 | 'method': 'POST', 10 | 'emits': ['parse.complete'], 11 | 'flows': ['parse-embed-rag'], 12 | } 13 | 14 | async def handler(req, ctx): 15 | input_url = req.body['url'] 16 | 17 | ctx.logger.info("Received URL: {}".format(input_url)) 18 | script_path = pathlib.Path().parent / "src" / "parse.py" 19 | 20 | try: 21 | subprocess.run(["python", str(script_path), input_url]) 22 | ctx.logger.info("Website was parsed, and text was saved") 23 | except Exception as e: 24 | ctx.logger.info(f"Error parsing website text: {e}") 25 | 26 | await ctx.emit({ 27 | 'topic': 'parse.complete', 28 | 'data': {'message': 'Parsing completed'} 29 | }) 30 | 31 | return { 32 | 'status': 200, 33 | 'body': {'status': "motia parse-chunk-embed-index initiated"} 34 | } 35 | 36 | -------------------------------------------------------------------------------- /examples/rag_example/steps/rag_api.step.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import pathlib 3 | import os 4 | 5 | src_path = pathlib.Path().parent / "src" 6 | sys.path.append(str(src_path)) 7 | 8 | from rag import rag_reponse 9 | 10 | config = { 11 | 'type': 'api', 12 | 'name': 'Rag API', 13 | 'description' : 'performs RAG for a given query and indexed data in temp', 14 | 'path': '/api/rag', 15 | 'method':'POST', 16 | 'emits':['rag.completed'], 17 | 'flows':['parse-embed-rag'], 18 | } 19 | 20 | async def handler(req, ctx): 21 | query = req.body['query'] 22 | ctx.logger.info("Received query: {}".format(query)) 23 | try: 24 | result = rag_reponse(query) 25 | message = "RAG response was provided to the user query : {}".format(query) 26 | ctx.logger.info(message) 27 | except: 28 | ctx.logger.info("Error responding to user query") 29 | 30 | await ctx.emit({ 31 | 'topic':'rag.completed', 32 | 'data':{'message':'rag response provided'} 33 | }) 34 | 35 | return{ 36 | 'status':200, 37 | 'body': {'answer':result} 38 | } -------------------------------------------------------------------------------- /examples/rag_example/temp/embeddings/embeddings_file.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MotiaDev/motia-examples/c1ea1854d55a9d75218e6c00a8302dcb98289bb8/examples/rag_example/temp/embeddings/embeddings_file.npy -------------------------------------------------------------------------------- /examples/rag_example/temp/faiss_files/vector_index.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MotiaDev/motia-examples/c1ea1854d55a9d75218e6c00a8302dcb98289bb8/examples/rag_example/temp/faiss_files/vector_index.bin -------------------------------------------------------------------------------- /examples/rag_example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "resolveJsonModule": true, 11 | "allowJs": true, 12 | "outDir": "dist", 13 | "rootDir": ".", 14 | "baseUrl": ".", 15 | "jsx": "react-jsx" 16 | }, 17 | "include": [ 18 | "**/*.ts", 19 | "**/*.tsx", 20 | "**/*.js", 21 | "**/*.jsx" 22 | ], 23 | "exclude": [ 24 | "node_modules", 25 | "dist", 26 | "tests" 27 | ] 28 | } -------------------------------------------------------------------------------- /examples/research-assistant/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | python_modules 3 | .venv 4 | venv 5 | .motia 6 | .mermaid 7 | dist 8 | *.pyc 9 | .env -------------------------------------------------------------------------------- /examples/research-assistant/data/workbench-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MotiaDev/motia-examples/c1ea1854d55a9d75218e6c00a8302dcb98289bb8/examples/research-assistant/data/workbench-image.png -------------------------------------------------------------------------------- /examples/research-assistant/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "research-assistant", 3 | "description": "Research Assistant for academic papers", 4 | "scripts": { 5 | "postinstall": "motia install", 6 | "dev": "motia dev", 7 | "dev:debug": "motia dev --debug", 8 | "workbench": "motia workbench", 9 | "clean": "rm -rf dist node_modules python_modules .motia .mermaid" 10 | }, 11 | "keywords": [ 12 | "motia" 13 | ], 14 | "dependencies": { 15 | "@google/generative-ai": "^0.24.1", 16 | "@motiadev/core": "0.1.0-beta.28", 17 | "@motiadev/workbench": "0.1.0-beta.28", 18 | "motia": "0.1.0-beta.28", 19 | "react": "^19.0.0", 20 | "zod": "^3.24.3" 21 | }, 22 | "devDependencies": { 23 | "@types/node": "^22.15.3", 24 | "@types/react": "^18.3.18", 25 | "ts-node": "^10.9.2", 26 | "typescript": "^5.7.3" 27 | } 28 | } -------------------------------------------------------------------------------- /examples/research-assistant/requirements.txt: -------------------------------------------------------------------------------- 1 | numpy==1.26.4 2 | pandas==2.2.1 3 | scikit-learn==1.4.1.post1 4 | matplotlib==3.8.3 5 | jupyter==1.0.0 6 | python-dotenv==1.0.1 7 | requests==2.31.0 -------------------------------------------------------------------------------- /examples/research-assistant/steps/extractText.step.ts: -------------------------------------------------------------------------------- 1 | export const config = { 2 | type: 'event', 3 | name: 'ExtractText', 4 | subscribes: ['paper-uploaded'], 5 | emits: ['text-extracted'], 6 | flows: ['research-assistant'] 7 | } 8 | 9 | export const handler = async (input: any, { emit }: { emit: any }) => { 10 | try { 11 | const { id, title, authors, abstract, pdfUrl, doi, uploadedAt } = input; 12 | 13 | console.log(`Extracting text from paper: ${title}`); 14 | 15 | 16 | const extractedText = abstract + "\n\n" + 17 | "This is simulated full text extraction from the PDF. " + 18 | "In a real implementation, this would contain the actual content of the paper. " + 19 | "The text would include all sections like introduction, methodology, results, " + 20 | "discussion, and conclusion."; 21 | 22 | await emit({ 23 | topic: 'text-extracted', 24 | data: { 25 | id, 26 | title, 27 | authors, 28 | abstract, 29 | pdfUrl, 30 | doi, 31 | uploadedAt, 32 | fullText: extractedText, 33 | extractedAt: new Date().toISOString() 34 | } 35 | }); 36 | 37 | console.log(`Text extracted from paper: ${title}`); 38 | 39 | } catch (error) { 40 | console.error('Error extracting text:', error); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/research-assistant/steps/generateMarkdownReport.step.ts: -------------------------------------------------------------------------------- 1 | import { generateMarkdownReport } from '../utils/generateMarkdownReport'; 2 | import path from 'path'; 3 | import fs from 'fs'; 4 | 5 | export const config = { 6 | type: 'event', 7 | name: 'GenerateMarkdownReport', 8 | subscribes: ['related-papers-recommended', 'code-examples-generated'], 9 | emits: ['markdown-report-generated'], 10 | flows: ['research-assistant'] 11 | } 12 | 13 | export const handler = async (input: any, { emit }: { emit: any }) => { 14 | try { 15 | const { id, title } = input; 16 | 17 | console.log(`Generating markdown report for paper: ${title}`); 18 | 19 | const knowledgeGraphPath = path.join(__dirname, '../data/knowledge-graph.json'); 20 | const outputPath = path.join(__dirname, '../data/research-report.md'); 21 | 22 | // Ensure knowledge graph exists 23 | if (!fs.existsSync(knowledgeGraphPath)) { 24 | console.error(`Knowledge graph file not found at: ${knowledgeGraphPath}`); 25 | return; 26 | } 27 | 28 | // Generate the markdown report 29 | generateMarkdownReport(knowledgeGraphPath, outputPath); 30 | 31 | // Verify the report was created 32 | if (fs.existsSync(outputPath)) { 33 | const stats = fs.statSync(outputPath); 34 | console.log(`Markdown report generated successfully. File size: ${stats.size} bytes`); 35 | 36 | // Read the first few lines to log for debugging 37 | const reportPreview = fs.readFileSync(outputPath, 'utf-8').split('\n').slice(0, 10).join('\n'); 38 | console.log(`Report preview:\n${reportPreview}\n...`); 39 | } else { 40 | console.warn(`Markdown report file was not created at: ${outputPath}`); 41 | } 42 | 43 | await emit({ 44 | topic: 'markdown-report-generated', 45 | data: { 46 | id, 47 | title, 48 | reportPath: outputPath, 49 | reportGeneratedAt: new Date().toISOString() 50 | } 51 | }); 52 | 53 | console.log(`Markdown report processing completed for paper: ${title}`); 54 | 55 | } catch (error) { 56 | const errorMessage = error instanceof Error ? error.message : String(error); 57 | console.error(`Error generating markdown report: ${errorMessage}`); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /examples/research-assistant/steps/human-review.step.ts: -------------------------------------------------------------------------------- 1 | import { NoopConfig } from 'motia'; 2 | 3 | export const config: NoopConfig = { 4 | type: 'noop', 5 | name: 'Human Review', 6 | description: 'Human researcher reviews and validates AI analysis', 7 | // Subscribe to the complete knowledge graph 8 | virtualSubscribes: ['knowledge-graph-updated'], 9 | // When human approves, emit these events 10 | virtualEmits: ['human-review-approved', 'human-review-rejected', 'human-review-feedback'], 11 | flows: ['research-assistant'] 12 | }; -------------------------------------------------------------------------------- /examples/research-assistant/steps/research-monitor.step.ts: -------------------------------------------------------------------------------- 1 | import { NoopConfig } from 'motia'; 2 | 3 | export const config: NoopConfig = { 4 | type: 'noop', 5 | name: 'Research Monitor', 6 | description: 'Monitors research paper analysis progress', 7 | virtualSubscribes: [ 8 | 'paper-analyzed', 9 | 'summary-generated', 10 | 'research-gaps-analyzed' 11 | ], 12 | virtualEmits: [ 13 | 'monitor-checkpoint', 14 | 'monitor-alert' 15 | ], 16 | flows: ['research-assistant'] 17 | }; -------------------------------------------------------------------------------- /examples/research-assistant/steps/research-monitor.step.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BaseHandle, Position, EventNodeProps } from '../node_modules/motia/dist/esm/workbench'; 3 | 4 | export default function ResearchMonitor(_: EventNodeProps) { 5 | return ( 6 |
7 |
Research Paper Monitor
8 |
Tracks analysis progress
9 | 10 |
11 |
12 |
13 | Paper Analysis 14 |
15 |
16 |
17 | Summary 18 |
19 |
20 |
21 | Research Gaps 22 |
23 |
24 | 25 | 26 | 27 |
28 | ); 29 | } -------------------------------------------------------------------------------- /examples/research-assistant/steps/serveCss.step.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as fs from 'fs'; 3 | 4 | export const config = { 5 | type: 'api', 6 | name: 'ServeCss', 7 | path: '/css/:filename', 8 | method: 'GET', 9 | emits: ['css-file-served'], 10 | flows: ['research-assistant'] 11 | } 12 | 13 | export const handler = async (request: any, { emit }: { emit: any }) => { 14 | try { 15 | const { filename } = request.params; 16 | console.log(`ServeCss: Serving CSS file ${filename}`); 17 | 18 | // Construct the file path 19 | const filePath = path.join(process.cwd(), 'public', 'css', filename); 20 | 21 | // Check if the file exists 22 | if (!fs.existsSync(filePath)) { 23 | console.error(`CSS file not found: ${filePath}`); 24 | return { 25 | status: 404, 26 | body: { error: 'CSS file not found' } 27 | }; 28 | } 29 | 30 | // Read the file 31 | const content = fs.readFileSync(filePath, 'utf8'); 32 | 33 | // Emit the event 34 | await emit({ 35 | topic: 'css-file-served', 36 | data: { 37 | path: `/css/${filename}`, 38 | servedAt: new Date().toISOString() 39 | } 40 | }); 41 | 42 | return { 43 | status: 200, 44 | headers: { 45 | 'Content-Type': 'text/css; charset=utf-8', 46 | 'Cache-Control': 'no-cache' 47 | }, 48 | body: content 49 | }; 50 | } catch (error) { 51 | console.error(`Error serving CSS file:`, error); 52 | return { 53 | status: 500, 54 | body: { error: 'Internal server error' } 55 | }; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /examples/research-assistant/steps/serveStatic.step.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as fs from 'fs'; 3 | 4 | export const config = { 5 | type: 'api', 6 | name: 'ServeStatic', 7 | path: '/js/:filename', 8 | method: 'GET', 9 | emits: ['static-file-served'], 10 | flows: ['research-assistant'] 11 | } 12 | 13 | export const handler = async (request: any, { emit }: { emit: any }) => { 14 | try { 15 | const { filename } = request.params; 16 | console.log(`ServeStatic: Serving static file ${filename}`); 17 | 18 | // Construct the file path 19 | const filePath = path.join(process.cwd(), 'public', 'js', filename); 20 | 21 | // Check if the file exists 22 | if (!fs.existsSync(filePath)) { 23 | console.error(`File not found: ${filePath}`); 24 | return { 25 | status: 404, 26 | body: { error: 'File not found' } 27 | }; 28 | } 29 | 30 | // Read the file 31 | const content = fs.readFileSync(filePath, 'utf8'); 32 | 33 | // Emit the event 34 | await emit({ 35 | topic: 'static-file-served', 36 | data: { 37 | path: `/js/${filename}`, 38 | servedAt: new Date().toISOString() 39 | } 40 | }); 41 | 42 | return { 43 | status: 200, 44 | headers: { 45 | 'Content-Type': 'application/javascript; charset=utf-8', 46 | 'Cache-Control': 'no-cache' 47 | }, 48 | body: content 49 | }; 50 | } catch (error) { 51 | console.error(`Error serving static file:`, error); 52 | return { 53 | status: 500, 54 | body: { error: 'Internal server error' } 55 | }; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /examples/research-assistant/steps/uploadPaper.step.ts: -------------------------------------------------------------------------------- 1 | export const config = { 2 | type: 'api', 3 | name: 'UploadPaper', 4 | path: '/api/upload-paper', 5 | method: 'POST', 6 | emits: ['paper-uploaded'], 7 | flows: ['research-assistant'] 8 | } 9 | 10 | export const handler = async (request: any, { emit }: { emit: any }) => { 11 | try { 12 | const { title, authors, abstract, pdfUrl, doi } = request.body; 13 | 14 | if (!title || !pdfUrl) { 15 | return { 16 | status: 400, 17 | body: { error: 'Title and PDF URL are required' } 18 | }; 19 | } 20 | 21 | const paperId = `paper-${Date.now()}`; 22 | 23 | await emit({ 24 | topic: 'paper-uploaded', 25 | data: { 26 | id: paperId, 27 | title, 28 | authors: authors || [], 29 | abstract: abstract || '', 30 | pdfUrl, 31 | doi: doi || '', 32 | uploadedAt: new Date().toISOString() 33 | } 34 | }); 35 | 36 | return { 37 | status: 200, 38 | body: { 39 | success: true, 40 | message: 'Paper uploaded successfully', 41 | paperId 42 | } 43 | }; 44 | } catch (error) { 45 | console.error('Error uploading paper:', error); 46 | return { 47 | status: 500, 48 | body: { error: 'Internal server error' } 49 | }; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/research-assistant/steps/webhook-simulator.step.ts: -------------------------------------------------------------------------------- 1 | import { NoopConfig } from 'motia'; 2 | 3 | export const config: NoopConfig = { 4 | type: 'noop', 5 | name: 'Paper Upload Webhook', 6 | description: 'Simulates incoming webhook events for paper uploads', 7 | virtualEmits: ['paper-uploaded'], 8 | virtualSubscribes: [], 9 | flows: ['research-assistant'], 10 | }; -------------------------------------------------------------------------------- /examples/research-assistant/steps/webhook-simulator.step.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BaseHandle, Position } from '../node_modules/motia/dist/esm/workbench'; 3 | 4 | export default function WebhookSimulator() { 5 | const simulateUpload = () => { 6 | fetch('/api/webhook/paper-upload', { 7 | method: 'POST', 8 | headers: { 'Content-Type': 'application/json' }, 9 | body: JSON.stringify({ 10 | event: 'paper-uploaded', 11 | timestamp: new Date().toISOString(), 12 | paperData: { 13 | title: 'Sample Research Paper', 14 | authors: ['Sample Author'], 15 | abstract: 'This is a sample paper abstract for testing.' 16 | } 17 | }), 18 | }); 19 | }; 20 | 21 | return ( 22 |
23 |
Paper Upload Webhook
24 |
Simulate paper uploads
25 | 26 | 32 | 33 | 34 |
35 | ); 36 | } -------------------------------------------------------------------------------- /examples/research-assistant/test-upload.js: -------------------------------------------------------------------------------- 1 | // Simple test script for the upload-paper endpoint 2 | // Using Node.js built-in fetch API (available in Node.js v20+) 3 | 4 | async function testUploadPaper() { 5 | try { 6 | const response = await fetch('http://localhost:3000/api/upload-paper', { 7 | method: 'POST', 8 | headers: { 9 | 'Content-Type': 'application/json', 10 | }, 11 | body: JSON.stringify({ 12 | title: 'Mem0', 13 | authors: 'Prateek Chhikara, Dev Khant, Saket Aryan, Taranjeet Singh, Deshraj Yadav', 14 | abstract: 'Large Language Models (LLMs) have demonstrated remarkable prowess in generating contextually coherent responses, yet their fixed context windows pose fundamental challenges for maintaining consistency over prolonged multi-session dialogues. We introduce Mem0, a scalable memory-centric architecture that addresses this issue by dynamically extracting, consolidating, and retrieving salient information from ongoing conversations.', 15 | pdfUrl: 'https://arxiv.org/pdf/2504.19413', 16 | doi: '2504.19413' 17 | }), 18 | }); 19 | 20 | const data = await response.json(); 21 | console.log('Response:', data); 22 | } catch (error) { 23 | console.error('Error:', error); 24 | } 25 | } 26 | 27 | testUploadPaper(); 28 | -------------------------------------------------------------------------------- /examples/research-assistant/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "resolveJsonModule": true, 11 | "allowJs": true, 12 | "outDir": "dist", 13 | "rootDir": ".", 14 | "baseUrl": ".", 15 | "jsx": "react-jsx" 16 | }, 17 | "include": [ 18 | "**/*.ts", 19 | "**/*.tsx", 20 | "**/*.js", 21 | "**/*.jsx" 22 | ], 23 | "exclude": [ 24 | "node_modules", 25 | "dist", 26 | "tests" 27 | ] 28 | } -------------------------------------------------------------------------------- /examples/research-assistant/types/knowledgeGraph.ts: -------------------------------------------------------------------------------- 1 | export interface RelatedPaper { 2 | title: string; 3 | authors: string; 4 | year: string; 5 | url: string | null; 6 | relevance: string; 7 | keyInsights: string[]; 8 | } 9 | 10 | export interface CodeExample { 11 | title: string; 12 | description: string; 13 | language: string; 14 | code: string; 15 | dependencies: string[]; 16 | usageNotes: string; 17 | } 18 | 19 | export interface CodeExamples { 20 | examples: CodeExample[]; 21 | } 22 | 23 | export interface Paper { 24 | id: string; 25 | title: string; 26 | authors?: string; 27 | abstract?: string; 28 | pdfUrl?: string; 29 | doi?: string; 30 | uploadedAt?: string; 31 | analyzedAt?: string; 32 | codeExamples?: CodeExamples; 33 | codeExamplesGeneratedAt?: string; 34 | internetRelatedPapers?: RelatedPaper[]; 35 | relatedPapersRecommendedAt?: string; 36 | } 37 | 38 | export interface Concept { 39 | name: string; 40 | description?: string; 41 | authors?: string; 42 | year?: string; 43 | url?: string | null; 44 | keyInsights?: string[]; 45 | papers: string[]; 46 | } 47 | 48 | export interface Relationship { 49 | source: string; 50 | target: string; 51 | type: string; 52 | relevance?: string; 53 | insights?: string[]; 54 | sharedConcepts?: string[]; 55 | strength?: number; 56 | } 57 | 58 | export interface KnowledgeGraph { 59 | papers: Record; 60 | concepts: Record; 61 | relationships: Relationship[]; 62 | entities: Record; 63 | } 64 | -------------------------------------------------------------------------------- /examples/trello-flow/.env.example: -------------------------------------------------------------------------------- 1 | TRELLO_API_KEY=your_trello_api_key 2 | TRELLO_TOKEN=your_trello_token 3 | 4 | OPENAI_API_KEY=your_openai_api_key 5 | OPENAI_MODEL=your_openai_model 6 | 7 | SLACK_WEBHOOK_URL=your_slack_webhook_url 8 | 9 | TRELLO_NEW_TASKS_LIST_ID=your_new_tasks_list_id 10 | TRELLO_IN_PROGRESS_LIST_ID=your_in_progress_list_id 11 | TRELLO_NEEDS_REVIEW_LIST_ID=your_needs_review_list_id 12 | TRELLO_COMPLETED_LIST_ID=your_completed_list_id 13 | 14 | # Custom Fields 15 | TRELLO_CUSTOM_FIELD_DEV_STATUS=your_development_status_field_id 16 | TRELLO_CUSTOM_FIELD_DONE_VALUE=your_done_value_id -------------------------------------------------------------------------------- /examples/trello-flow/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine 2 | 3 | ARG NPM_TOKEN 4 | 5 | ENV NPM_TOKEN=${NPM_TOKEN} 6 | 7 | WORKDIR /app 8 | 9 | COPY package*.json ./ 10 | COPY pnpm-lock.yaml ./ 11 | 12 | RUN echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc 13 | 14 | RUN npm install -g pnpm && pnpm install 15 | 16 | RUN rm -f .npmrc 17 | 18 | COPY . . 19 | 20 | CMD ["pnpm", "dev"] -------------------------------------------------------------------------------- /examples/trello-flow/config/default.ts: -------------------------------------------------------------------------------- 1 | import { env } from './env' 2 | 3 | export const appConfig = { 4 | trello: { 5 | apiKey: env.TRELLO_API_KEY, 6 | token: env.TRELLO_TOKEN, 7 | lists: { 8 | newTasks: env.TRELLO_NEW_TASKS_LIST_ID, 9 | inProgress: env.TRELLO_IN_PROGRESS_LIST_ID, 10 | needsReview: env.TRELLO_NEEDS_REVIEW_LIST_ID, 11 | completed: env.TRELLO_COMPLETED_LIST_ID, 12 | }, 13 | customFields: { 14 | developmentStatus: env.TRELLO_CUSTOM_FIELD_DEV_STATUS!, 15 | doneValue: env.TRELLO_CUSTOM_FIELD_DONE_VALUE!, 16 | }, 17 | }, 18 | slack: { 19 | webhookUrl: env.SLACK_WEBHOOK_URL, 20 | }, 21 | openai: { 22 | apiKey: env.OPENAI_API_KEY, 23 | model: env.OPENAI_MODEL, 24 | }, 25 | } 26 | -------------------------------------------------------------------------------- /examples/trello-flow/config/env.ts: -------------------------------------------------------------------------------- 1 | import { cleanEnv, str } from 'envalid' 2 | 3 | export const env = cleanEnv(process.env, { 4 | TRELLO_API_KEY: str({ devDefault: 'trello-api-key' }), 5 | TRELLO_TOKEN: str({ devDefault: 'trello-token' }), 6 | 7 | OPENAI_API_KEY: str({ devDefault: 'openai-api-key' }), 8 | OPENAI_MODEL: str({ devDefault: 'gpt-3.5-turbo' }), 9 | 10 | SLACK_WEBHOOK_URL: str({ devDefault: 'slack-webhook-url' }), 11 | 12 | TRELLO_NEW_TASKS_LIST_ID: str({ devDefault: 'new-tasks-list-id' }), 13 | TRELLO_IN_PROGRESS_LIST_ID: str({ devDefault: 'in-progress-list-id' }), 14 | TRELLO_NEEDS_REVIEW_LIST_ID: str({ devDefault: 'needs-review-list-id' }), 15 | TRELLO_COMPLETED_LIST_ID: str({ devDefault: 'completed-list-id' }), 16 | 17 | TRELLO_CUSTOM_FIELD_DEV_STATUS: str({ devDefault: 'custom-field-dev-status-id' }), 18 | TRELLO_CUSTOM_FIELD_DONE_VALUE: str({ devDefault: 'custom-field-done-value-id' }), 19 | }) 20 | -------------------------------------------------------------------------------- /examples/trello-flow/docs/images/trello-manager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MotiaDev/motia-examples/c1ea1854d55a9d75218e6c00a8302dcb98289bb8/examples/trello-flow/docs/images/trello-manager.png -------------------------------------------------------------------------------- /examples/trello-flow/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | transform: { 5 | '^.+\\.[tj]sx?$': 'ts-jest', 6 | }, 7 | transformIgnorePatterns: ['/node_modules/(?!.*@motiadev)', '/node_modules/(?!.*motia)'], 8 | moduleNameMapper: { 9 | '^@motiadev/test(.*)$': '/node_modules/@motiadev/test/dist$1', 10 | '^motia(.*)$': '/node_modules/motia/dist/cjs$1', 11 | }, 12 | setupFiles: ['/jest.setup.js'], 13 | testMatch: ['**/__tests__/**/*.test.ts'], 14 | moduleFileExtensions: ['ts', 'js'], 15 | clearMocks: true, 16 | resetMocks: true, 17 | } 18 | -------------------------------------------------------------------------------- /examples/trello-flow/jest.setup.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | require('dotenv').config({ 3 | path: path.resolve(__dirname, '.env.test'), 4 | }) 5 | 6 | process.env.NODE_ENV = 'test' 7 | -------------------------------------------------------------------------------- /examples/trello-flow/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-motia-project", 3 | "description": "", 4 | "version": "0.0.1", 5 | "type": "commonjs", 6 | "scripts": { 7 | "dev": "NODE_ENV=development motia dev", 8 | "dev:debug": "motia dev --debug", 9 | "generate:config": "motia get-config --output ./", 10 | "test": "NODE_ENV=test jest", 11 | "test:watch": "NODE_ENV=test jest --watch", 12 | "test:coverage": "NODE_ENV=test jest --coverage", 13 | "test:coverage:report": "NODE_ENV=test jest --coverage && open coverage/lcov-report/index.html", 14 | "lint": "eslint . --ext .ts", 15 | "lint:fix": "eslint . --ext .ts --fix", 16 | "prettier:fix": "prettier --write **/*.{js,ts,jsx,tsx}" 17 | }, 18 | "keywords": [ 19 | "motia" 20 | ], 21 | "dependencies": { 22 | "@radix-ui/react-icons": "^1.3.2", 23 | "@radix-ui/react-select": "^2.1.6", 24 | "axios": "^1.6.0", 25 | "deep-equal": "^2.2.0", 26 | "envalid": "^8.0.0", 27 | "motia": "^0.1.0-beta.15", 28 | "openai": "^4.83.0", 29 | "react": "^19.0.0", 30 | "zod": "^3.24.1" 31 | }, 32 | "devDependencies": { 33 | "@motiadev/test": "^0.1.0-beta.15", 34 | "@types/jest": "^29.5.14", 35 | "@types/react": "^18.3.18", 36 | "@typescript-eslint/eslint-plugin": "^6.21.0", 37 | "@typescript-eslint/parser": "^6.21.0", 38 | "dotenv": "^16.4.5", 39 | "eslint": "^8.57.0", 40 | "eslint-config-prettier": "^9.1.0", 41 | "eslint-plugin-prettier": "^5.1.3", 42 | "jest": "^29.7.0", 43 | "prettier": "^3.2.5", 44 | "ts-jest": "^29.2.5", 45 | "ts-node": "^10.9.2", 46 | "typescript": "^5.7.3" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /examples/trello-flow/services/__tests__/slack.service.test.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import {Logger} from "motia"; 3 | import {createMockLogger} from "@motiadev/test"; 4 | import {SlackService} from '../slack.service' 5 | 6 | jest.mock('axios') 7 | jest.mock('motia') 8 | 9 | describe('SlackService', () => { 10 | let slackService: SlackService 11 | const mockWebhookUrl = 'https://hooks.slack.com/test' 12 | let mockLogger: jest.Mocked 13 | 14 | beforeEach(() => { 15 | mockLogger = createMockLogger() 16 | 17 | slackService = new SlackService(mockWebhookUrl, mockLogger) 18 | ;(axios.post as jest.Mock).mockClear() 19 | }) 20 | 21 | describe('sendMessage', () => { 22 | it('should send a message successfully', async () => { 23 | ;(axios.post as jest.Mock).mockResolvedValue({ data: 'ok' }) 24 | 25 | await slackService.sendMessage('#test-channel', 'Test message') 26 | 27 | expect(axios.post).toHaveBeenCalledWith(mockWebhookUrl, { 28 | channel: '#test-channel', 29 | text: 'Test message', 30 | }) 31 | }) 32 | 33 | it('should handle network errors', async () => { 34 | const mockError = new Error('Network error') 35 | ;(axios.post as jest.Mock).mockRejectedValue(mockError) 36 | 37 | await expect(slackService.sendMessage('#test-channel', 'Test message')).rejects.toThrow('Network error') 38 | expect(mockLogger.error).toHaveBeenCalledWith('Error sending Slack message', mockError) 39 | }) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /examples/trello-flow/services/openai.service.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from 'openai' 2 | import { Logger } from 'motia' 3 | import { appConfig } from '../config/default' 4 | 5 | export class OpenAIService { 6 | private openai: OpenAI 7 | private logger: Logger 8 | 9 | constructor(logger: Logger) { 10 | this.openai = new OpenAI({ 11 | apiKey: appConfig.openai.apiKey, 12 | }) 13 | this.logger = logger 14 | } 15 | 16 | async generateSummary(title: string, description: string): Promise { 17 | this.logger.info('Generating summary for task', { title }) 18 | 19 | // complete prompt suggestion 20 | // const prompt = `Generate a concise, professional technical summary of this Trello ticket. 21 | // Follow these guidelines: 22 | // - Capture the core technical objective 23 | // - Highlight key implementation details 24 | // - Use clear, precise language 25 | // - Focus on the problem being solved 26 | // - Maintain a neutral, professional tone 27 | 28 | // Ticket Details: 29 | // Title: ${title} 30 | // Description: ${description} 31 | 32 | // Summary Format: 33 | // - Start with the primary technical goal 34 | // - Briefly explain the technical approach or solution 35 | // - Mention any critical constraints or considerations 36 | 37 | // Output a single, crisp sentence that a senior engineer would find informative.` 38 | 39 | const prompt = `Summarize the following Trello task: 40 | Title: ${title} 41 | Description: ${description} 42 | 43 | Summarize in one sentence.` 44 | 45 | try { 46 | const completion = await this.openai.chat.completions.create({ 47 | model: appConfig.openai.model, 48 | messages: [ 49 | { 50 | role: 'user', 51 | content: prompt, 52 | }, 53 | ], 54 | temperature: 0.5, 55 | max_tokens: 50, 56 | }) 57 | 58 | const summary = completion.choices[0]?.message?.content?.trim() || 'No summary generated' 59 | this.logger.info('Summary generated successfully') 60 | 61 | return summary 62 | } catch (error) { 63 | this.logger.error('Failed to generate summary', { error }) 64 | throw new Error('Unable to generate summary at this time') 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /examples/trello-flow/services/slack.service.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { Logger } from 'motia' 3 | 4 | export class SlackService { 5 | private logger: Logger 6 | 7 | constructor(private webhookUrl: string, logger: Logger) { 8 | this.logger = logger 9 | } 10 | 11 | async sendMessage(channel: string, message: string) { 12 | try { 13 | await axios.post(this.webhookUrl, { 14 | channel, 15 | text: message, 16 | }) 17 | } catch (error) { 18 | this.logger.error('Error sending Slack message', error) 19 | throw error 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/trello-flow/steps/__tests__/check-overdue-cards.test.ts: -------------------------------------------------------------------------------- 1 | import { createMockContext, MockFlowContext } from "@motiadev/test"; 2 | import { TrelloService } from '../../services/trello.service' 3 | import { handler } from '../check-overdue-cards.step' 4 | 5 | jest.mock('motia') 6 | jest.mock('../../services/trello.service') 7 | 8 | describe('Check Overdue Cards', () => { 9 | let mockContext: MockFlowContext 10 | let mockGetCardsInList: jest.Mock 11 | let mockAddComment: jest.Mock 12 | 13 | beforeEach(() => { 14 | mockContext = createMockContext() 15 | mockGetCardsInList = jest.fn() 16 | mockAddComment = jest.fn() 17 | 18 | ;(TrelloService as jest.Mock).mockImplementation(() => ({ 19 | getCardsInList: mockGetCardsInList, 20 | addComment: mockAddComment, 21 | })) 22 | 23 | jest.clearAllMocks() 24 | }) 25 | 26 | it('should identify and comment on overdue cards', async () => { 27 | const overdueCard = { 28 | id: 'card-123', 29 | name: 'Overdue Card', 30 | due: '2023-01-01T00:00:00.000Z', 31 | } 32 | 33 | mockGetCardsInList.mockResolvedValue([overdueCard]) 34 | 35 | await handler(mockContext) 36 | 37 | expect(mockAddComment).toHaveBeenCalledWith( 38 | 'card-123', 39 | '⚠️ OVERDUE: This card has passed its due date!' 40 | ) 41 | }) 42 | 43 | it('should not comment on cards without due date', async () => { 44 | const cardWithoutDue = { 45 | id: 'card-123', 46 | name: 'Normal Card', 47 | due: null, 48 | } 49 | 50 | mockGetCardsInList.mockResolvedValue([cardWithoutDue]) 51 | 52 | await handler(mockContext) 53 | 54 | expect(mockAddComment).not.toHaveBeenCalled() 55 | }) 56 | }) -------------------------------------------------------------------------------- /examples/trello-flow/steps/__tests__/mark-card-for-review.test.ts: -------------------------------------------------------------------------------- 1 | import { createMockContext } from '@motiadev/test' 2 | import { TrelloService } from '../../services/trello.service' 3 | import { OpenAIService } from '../../services/openai.service' 4 | import { handler } from '../mark-card-for-review.step' 5 | import { appConfig } from '../../config/default' 6 | 7 | jest.mock('../../services/trello.service') 8 | jest.mock('../../services/openai.service') 9 | jest.mock('../../config/default', () => ({ 10 | appConfig: { 11 | trello: { 12 | lists: { 13 | inProgress: 'list-123', 14 | needsReview: 'list-456', 15 | }, 16 | }, 17 | }, 18 | })) 19 | 20 | describe('Mark Card For Review', () => { 21 | const mockContext = createMockContext() 22 | const mockMoveCard = jest.fn() 23 | const mockGenerateSummary = jest.fn() 24 | const mockGetCard = jest.fn() 25 | 26 | beforeEach(() => { 27 | jest.clearAllMocks() 28 | ;(TrelloService as jest.Mock).mockImplementation(() => ({ 29 | moveCard: mockMoveCard, 30 | getCard: mockGetCard, 31 | addComment: jest.fn(), 32 | })) 33 | ;(OpenAIService as jest.Mock).mockImplementation(() => ({ 34 | generateSummary: mockGenerateSummary, 35 | })) 36 | }) 37 | 38 | it('should move card to needs review when development is completed', async () => { 39 | const mockCard = { 40 | id: 'card-123', 41 | name: 'Test Card', 42 | desc: 'Card description', 43 | idList: appConfig.trello.lists.inProgress, 44 | } 45 | const mockSummary = 'Generated summary' 46 | 47 | mockGetCard.mockResolvedValue(mockCard) 48 | mockGenerateSummary.mockResolvedValue(mockSummary) 49 | 50 | const input = { 51 | id: 'card-123', 52 | customFieldItem: { 53 | idCustomField: '67a761df9d19dc4a6506eb75', 54 | idValue: '67a76e7cf356bd5af7d8b744', 55 | }, 56 | } 57 | 58 | await handler(input, mockContext) 59 | 60 | expect(mockMoveCard).toHaveBeenCalledWith('card-123', appConfig.trello.lists.needsReview) 61 | expect(mockContext.emit).toHaveBeenCalledWith({ 62 | topic: 'notify.slack', 63 | data: { 64 | channel: '#code-review', 65 | message: expect.stringContaining(mockSummary), 66 | }, 67 | }) 68 | }) 69 | }) -------------------------------------------------------------------------------- /examples/trello-flow/steps/__tests__/slack-notifier.test.ts: -------------------------------------------------------------------------------- 1 | import { createMockContext } from '@motiadev/test' 2 | import { handler } from '../slack-notifier.step' 3 | import { SlackService } from '../../services/slack.service' 4 | 5 | jest.mock('../../services/slack.service') 6 | 7 | describe('Slack Notifier Handler', () => { 8 | const mockSendMessage = jest.fn() 9 | const mockContext = createMockContext() 10 | 11 | beforeEach(() => { 12 | jest.clearAllMocks() 13 | ;(SlackService as jest.Mock).mockImplementation(() => ({ 14 | sendMessage: mockSendMessage, 15 | })) 16 | }) 17 | 18 | it('should send message to specified channel', async () => { 19 | const notification = { 20 | channel: 'test-channel', 21 | message: 'Test message', 22 | } 23 | 24 | await handler(notification, mockContext) 25 | 26 | expect(mockSendMessage).toHaveBeenCalledWith('test-channel', 'Test message') 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /examples/trello-flow/steps/check-overdue-cards.step.ts: -------------------------------------------------------------------------------- 1 | import { CronConfig, FlowContext } from 'motia' 2 | import { TrelloService } from '../services/trello.service' 3 | import { appConfig } from '../config/default' 4 | 5 | export const config: CronConfig = { 6 | type: 'cron', 7 | name: 'Check Overdue Cards', 8 | description: 'Identifies and flags cards that have passed their due date', 9 | cron: '0 * * * *', 10 | emits: [], 11 | flows: ['trello'], 12 | } 13 | 14 | export const handler = async ({ logger }: FlowContext) => { 15 | const trello = new TrelloService(appConfig.trello, logger) 16 | logger.info('Starting overdue task check') 17 | 18 | try { 19 | const listsToCheck = [ 20 | appConfig.trello.lists.newTasks, 21 | appConfig.trello.lists.inProgress, 22 | appConfig.trello.lists.needsReview, 23 | ] 24 | 25 | for (const listId of listsToCheck) { 26 | const cards = await trello.getCardsInList(listId) 27 | 28 | for (const card of cards) { 29 | if (card.due && new Date(card.due) < new Date()) { 30 | logger.info('Found overdue card', { cardId: card.id, name: card.name }) 31 | await trello.addComment(card.id, '⚠️ OVERDUE: This card has passed its due date!') 32 | } 33 | } 34 | } 35 | 36 | logger.info('Completed overdue task check') 37 | } catch (error) { 38 | logger.error('Error checking overdue tasks', error) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/trello-flow/steps/complete-approved-card.step.ts: -------------------------------------------------------------------------------- 1 | import { EventConfig, StepHandler } from 'motia' 2 | import { z } from 'zod' 3 | import { TrelloService } from '../services/trello.service' 4 | import { appConfig } from '../config/default' 5 | 6 | const inputSchema = z.object({ 7 | card: z.object({ 8 | id: z.string(), 9 | list: z.object({ 10 | id: z.string(), 11 | }), 12 | }), 13 | comment: z.object({ 14 | text: z.string(), 15 | idMember: z.string(), 16 | }), 17 | }) 18 | 19 | export const config: EventConfig = { 20 | type: 'event', 21 | name: 'Complete Approved Card', 22 | description: 'Moves approved cards to the completed state', 23 | subscribes: ['card.commented', 'card.reviewCompleted'], 24 | emits: [], 25 | input: inputSchema, 26 | flows: ['trello'], 27 | } 28 | 29 | export const handler: StepHandler = async (payload, { logger }) => { 30 | const trelloService = new TrelloService(appConfig.trello, logger) 31 | const { card, comment } = payload 32 | 33 | logger.info('Processing completion request', { 34 | cardId: card.id, 35 | commentText: comment.text, 36 | listId: card.list.id, 37 | }) 38 | 39 | if (comment.text.toLowerCase() === 'approved' && card.list.id === appConfig.trello.lists.needsReview) { 40 | 41 | logger.info('Moving card to Completed', { cardId: card.id }) 42 | await trelloService.moveCard(card.id, appConfig.trello.lists.completed) 43 | 44 | await trelloService.addComment(card.id, `✅ Card has been approved by @${comment.idMember}. Moving to Completed!`) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/trello-flow/steps/mark-card-for-review.step.ts: -------------------------------------------------------------------------------- 1 | import { EventConfig, StepHandler } from 'motia' 2 | import { z } from 'zod' 3 | import { TrelloService } from '../services/trello.service' 4 | import { OpenAIService } from '../services/openai.service' 5 | import { appConfig } from '../config/default' 6 | 7 | const inputSchema = z.object({ 8 | id: z.string(), 9 | customFieldItem: z 10 | .object({ 11 | idCustomField: z.string(), 12 | idValue: z.string().nullable(), 13 | }) 14 | .optional(), 15 | }) 16 | 17 | export const config: EventConfig = { 18 | type: 'event', 19 | name: 'Mark Card For Review', 20 | description: 'Moves completed cards to review queue and notifies reviewers', 21 | subscribes: ['card.updateCustomFieldItem', 'card.developmentCompleted'], 22 | emits: ['notify.slack'], 23 | virtualEmits: ['card.needsReview'], 24 | input: inputSchema, 25 | flows: ['trello'], 26 | } 27 | 28 | export const handler: StepHandler = async (input, { emit, logger }) => { 29 | logger.info('Needs Review Handler', { input }) 30 | 31 | const trelloService = new TrelloService(appConfig.trello, logger) 32 | const openaiService = new OpenAIService(logger) 33 | 34 | const card = await trelloService.getCard(input.id) 35 | 36 | const cardIsReadyForReview = 37 | input.customFieldItem?.idCustomField === '67a761df9d19dc4a6506eb75' && 38 | input.customFieldItem?.idValue === '67a76e7cf356bd5af7d8b744' 39 | 40 | if (cardIsReadyForReview && card.idList === appConfig.trello.lists.inProgress) { 41 | logger.info('Moving card to Needs Review', { cardId: card.id }) 42 | 43 | await trelloService.moveCard(card.id, appConfig.trello.lists.needsReview) 44 | 45 | const summary = await openaiService.generateSummary(card.name, card.desc) 46 | await trelloService.addComment(card.id, `🔍 Task is ready for review!\n📝 Summary below: \n${summary}`) 47 | 48 | await emit({ 49 | topic: 'notify.slack', 50 | data: { 51 | channel: '#code-review', 52 | message: `New task ready for review: ${card.name}\n${summary}`, 53 | }, 54 | }) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /examples/trello-flow/steps/noops/development-completed.step.ts: -------------------------------------------------------------------------------- 1 | import { NoopConfig } from 'motia' 2 | 3 | export const config: NoopConfig = { 4 | type: 'noop', 5 | name: 'Card Development Completed', 6 | description: 'Development is completed and card is ready for review', 7 | virtualEmits: ['card.developmentCompleted'], 8 | virtualSubscribes: ['card.inProgress'], 9 | flows: ['trello'], 10 | } 11 | -------------------------------------------------------------------------------- /examples/trello-flow/steps/noops/review-completed.step.ts: -------------------------------------------------------------------------------- 1 | import { NoopConfig } from 'motia' 2 | 3 | export const config: NoopConfig = { 4 | type: 'noop', 5 | name: 'Card Review Completed', 6 | description: 'Card has been reviewed and approved', 7 | virtualEmits: ['card.reviewCompleted'], 8 | virtualSubscribes: ['card.needsReview'], 9 | flows: ['trello'], 10 | } 11 | -------------------------------------------------------------------------------- /examples/trello-flow/steps/noops/trello-webhook-simulator.step.ts: -------------------------------------------------------------------------------- 1 | import { NoopConfig } from 'motia' 2 | 3 | export const config: NoopConfig = { 4 | type: 'noop', 5 | name: 'Trello Webhook Simulator', 6 | description: 'This node is used to simulate a Trello webhook.', 7 | virtualEmits: ['api.trello.webhook'], 8 | virtualSubscribes: [], 9 | flows: ['trello'], 10 | } 11 | -------------------------------------------------------------------------------- /examples/trello-flow/steps/noops/validation-completed.step.ts: -------------------------------------------------------------------------------- 1 | import { NoopConfig } from 'motia' 2 | 3 | export const config: NoopConfig = { 4 | type: 'noop', 5 | name: 'Card Validation Completed', 6 | description: 'Card has been validated and is ready for development', 7 | virtualEmits: ['card.readyForDevelopment'], 8 | virtualSubscribes: ['card.validated'], 9 | flows: ['trello'], 10 | } 11 | -------------------------------------------------------------------------------- /examples/trello-flow/steps/slack-notifier.step.ts: -------------------------------------------------------------------------------- 1 | import { EventConfig, StepHandler } from 'motia' 2 | import { z } from 'zod' 3 | import { SlackService } from '../services/slack.service' 4 | import { appConfig } from '../config/default' 5 | 6 | const inputSchema = z.object({ 7 | channel: z.string(), 8 | message: z.string(), 9 | }) 10 | 11 | export const config: EventConfig = { 12 | type: 'event', 13 | name: 'Slack Notifier', 14 | description: 'Sends notifications to Slack channels', 15 | subscribes: ['notify.slack'], 16 | emits: [], 17 | input: inputSchema, 18 | flows: ['trello'], 19 | } 20 | 21 | export const handler: StepHandler = async (notification, { logger }) => { 22 | logger.info('Sending notification to Slack', { notification }) 23 | const slack = new SlackService(appConfig.slack.webhookUrl, logger) 24 | await slack.sendMessage(notification.channel, notification.message) 25 | } 26 | -------------------------------------------------------------------------------- /examples/trello-flow/steps/start-assigned-card.step.ts: -------------------------------------------------------------------------------- 1 | import { EventConfig, StepHandler } from 'motia' 2 | import { z } from 'zod' 3 | import { TrelloService } from '../services/trello.service' 4 | import { appConfig } from '../config/default' 5 | 6 | const inputSchema = z.object({ 7 | id: z.string(), 8 | }) 9 | 10 | export const config: EventConfig = { 11 | type: 'event', 12 | name: 'Start Assigned Card', 13 | description: 'Moves newly assigned cards to the in-progress state', 14 | subscribes: ['member.assigned', 'card.readyForDevelopment'], 15 | virtualEmits: ['card.inProgress'], 16 | emits: [''], 17 | input: inputSchema, 18 | flows: ['trello'], 19 | } 20 | 21 | export const handler: StepHandler = async (payload, { logger }) => { 22 | try { 23 | logger.info('Start Assigned Card Handler', { payload }) 24 | const trelloService = new TrelloService(appConfig.trello, logger) 25 | const card = await trelloService.getCard(payload.id) 26 | 27 | if (card.idList === appConfig.trello.lists.newTasks && card.members.length > 0) { 28 | const [firstMember] = card.members 29 | 30 | logger.info('Moving card to In Progress', { 31 | cardId: card.id, 32 | member: firstMember.fullName, 33 | }) 34 | 35 | await trelloService.moveCard(card.id, appConfig.trello.lists.inProgress) 36 | 37 | await trelloService.addComment( 38 | card.id, 39 | `🚀 Card has been assigned to **${firstMember.fullName}** and moved to In Progress!`, 40 | ) 41 | } 42 | } catch (error) { 43 | logger.error('Error in Task Progress Handler', error) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/trello-flow/steps/trello-webhook-validation.step.ts: -------------------------------------------------------------------------------- 1 | import { ApiRouteConfig, ApiRequest, FlowContext } from 'motia' 2 | import { z } from 'zod' 3 | 4 | const inputSchema = z.object({}) 5 | 6 | export const config: ApiRouteConfig = { 7 | type: 'api', 8 | name: 'Trello Webhook Validation', 9 | description: 'Validates incoming Trello webhook connection', 10 | path: '/trello/webhook', 11 | method: 'HEAD', 12 | bodySchema: inputSchema, 13 | emits: [{ topic: 'api.trello.webhook', label: 'Trello Webhook Validation' }], 14 | virtualEmits: [{ topic: 'api.trello.webhook', label: 'Trello Webhook Validation' }], 15 | flows: ['trello'], 16 | } 17 | 18 | export const handler = async (request: ApiRequest, context: FlowContext) => { 19 | context.logger.info('Trello webhook validation request received') 20 | 21 | return { 22 | status: 200, 23 | body: { message: 'Webhook validation successful' }, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/trello-flow/steps/validate-card-requirements.step.ts: -------------------------------------------------------------------------------- 1 | import { EventConfig, StepHandler } from 'motia' 2 | import { z } from 'zod' 3 | import { appConfig } from '../config/default' 4 | import { TrelloService } from '../services/trello.service' 5 | 6 | const inputSchema = z 7 | .object({ 8 | id: z.string(), 9 | name: z.string().min(1, { message: 'Title is required' }), 10 | desc: z.string().min(1, { message: 'Description is required' }), 11 | members: z.array(z.object({})).min(1, { message: 'At least one assigned user is required' }), 12 | }) 13 | .strict() 14 | 15 | export const config: EventConfig = { 16 | type: 'event', 17 | name: 'Card Requirements Validator', 18 | description: 'Ensures new cards have required title, description and assignee', 19 | subscribes: ['card.created'], 20 | emits: [''], 21 | virtualEmits: ['card.validated'], 22 | input: inputSchema, 23 | flows: ['trello'], 24 | } 25 | 26 | export const handler: StepHandler = async (card, { logger }) => { 27 | logger.info('New task validator', { card }) 28 | const trello = new TrelloService(appConfig.trello, logger) 29 | 30 | try { 31 | inputSchema.parse(card) 32 | logger.info('Card validation successful', { cardId: card.id }) 33 | } catch (error) { 34 | if (error instanceof z.ZodError) { 35 | const missingFields = error.errors 36 | .map((err) => { 37 | const fieldMap: Record = { 38 | name: 'title', 39 | desc: 'description', 40 | members: 'assigned user', 41 | } 42 | 43 | const path = err.path[0] as string 44 | return fieldMap[path] || path 45 | }) 46 | .filter(Boolean) 47 | 48 | logger.info('Adding comment for missing fields', { cardId: card.id, missingFields }) 49 | await Promise.all([ 50 | trello.addComment(card.id, `🚨 Card is incomplete! Please add: \n* ${missingFields.join('\n* ')}`), 51 | trello.moveCard(card.id, appConfig.trello.lists.newTasks), 52 | ]) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /examples/trello-flow/test/mocks/trello-card.mock.ts: -------------------------------------------------------------------------------- 1 | import { TrelloCard } from '../../types/trello' 2 | 3 | export const createMockTrelloCard = (overrides: Partial = {}): TrelloCard => ({ 4 | id: overrides.id || 'card123', 5 | name: overrides.name || 'Test Card', 6 | desc: overrides.desc || 'Test card description', 7 | idList: overrides.idList || 'list123', 8 | due: overrides.due, 9 | dueComplete: overrides.dueComplete ?? false, 10 | members: overrides.members || [], 11 | }) 12 | 13 | export const createOverdueMockCard = (overrides: Partial = {}): TrelloCard => 14 | createMockTrelloCard({ 15 | id: 'card2', 16 | name: 'Overdue Task', 17 | due: new Date(Date.now() - 86400000).toISOString(), // due yesterday 18 | ...overrides, 19 | }) 20 | 21 | export const createFutureDueMockCard = (overrides: Partial = {}): TrelloCard => 22 | createMockTrelloCard({ 23 | id: 'card1', 24 | name: 'Future Task', 25 | due: new Date(Date.now() + 86400000).toISOString(), // due tomorrow 26 | ...overrides, 27 | }) 28 | 29 | export const mockCards: TrelloCard[] = [createFutureDueMockCard(), createOverdueMockCard()] 30 | -------------------------------------------------------------------------------- /examples/trello-flow/test/test-helpers.ts: -------------------------------------------------------------------------------- 1 | import { globalLogger, Logger, FlowContext, InternalStateManager } from 'motia' 2 | 3 | export const createMockLogger = () => { 4 | const mockLogger = globalLogger.child({ traceId: 'test-trace-id' }) as jest.Mocked 5 | return mockLogger 6 | } 7 | 8 | export const setupLoggerMock = () => { 9 | ;(Logger as jest.MockedClass).mockImplementation( 10 | () => ({ info: jest.fn(), debug: jest.fn(), warn: jest.fn(), error: jest.fn(), log: jest.fn() }) as any, 11 | ) 12 | } 13 | 14 | export const createMockContext = (logger = createMockLogger(), emit = jest.fn()): FlowContext => { 15 | return { 16 | logger, 17 | emit, 18 | traceId: 'test-trace-id', 19 | state: { 20 | get: jest.fn(), 21 | set: jest.fn(), 22 | delete: jest.fn(), 23 | clear: jest.fn(), 24 | } as InternalStateManager, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/trello-flow/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "lib": ["ES2022"], 7 | "outDir": "dist", 8 | "rootDir": ".", 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "resolveJsonModule": true, 14 | "baseUrl": ".", 15 | "paths": { 16 | "*": ["*"] 17 | } 18 | }, 19 | "include": ["**/*.ts"], 20 | "exclude": ["node_modules", "dist", "__tests__"] 21 | } 22 | -------------------------------------------------------------------------------- /examples/vision-example/.env.example: -------------------------------------------------------------------------------- 1 | FAL_API_KEY= -------------------------------------------------------------------------------- /examples/vision-example/.python-version: -------------------------------------------------------------------------------- 1 | 3.11.10 2 | -------------------------------------------------------------------------------- /examples/vision-example/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /examples/vision-example/docs/images/eval-agent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MotiaDev/motia-examples/c1ea1854d55a9d75218e6c00a8302dcb98289bb8/examples/vision-example/docs/images/eval-agent.png -------------------------------------------------------------------------------- /examples/vision-example/docs/images/generate-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MotiaDev/motia-examples/c1ea1854d55a9d75218e6c00a8302dcb98289bb8/examples/vision-example/docs/images/generate-image.png -------------------------------------------------------------------------------- /examples/vision-example/generate-dataset.ts: -------------------------------------------------------------------------------- 1 | async function generateImages(numImages: number = 10) { 2 | const prompt = "create an image of a couple backpacking through a trail in the easter sierras. use a black and white image style. sketch style."; 3 | 4 | const requests = Array(numImages).fill(null).map(() => 5 | fetch('http://localhost:3000/generate-image', { 6 | method: 'POST', 7 | headers: { 8 | 'Content-Type': 'application/json', 9 | }, 10 | body: JSON.stringify({ prompt }), 11 | }).then(res => res.json()) 12 | ); 13 | 14 | try { 15 | const results = await Promise.all(requests); 16 | console.log(`Successfully generated ${results.length} images`); 17 | return results; 18 | } catch (error) { 19 | console.error('Error generating images:', error); 20 | throw error; 21 | } 22 | } 23 | 24 | generateImages() 25 | .then(results => console.log('All images generated:', results)) 26 | .catch(error => console.error('Failed to generate images:', error)); 27 | -------------------------------------------------------------------------------- /examples/vision-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-motia-project", 3 | "description": "", 4 | "scripts": { 5 | "generate:dataset": "ts-node generate-dataset.ts", 6 | "dev": "motia dev", 7 | "dev:debug": "motia dev --debug", 8 | "generate:config": "motia get-config --output ./" 9 | }, 10 | "keywords": [ 11 | "motia" 12 | ], 13 | "dependencies": { 14 | "@fal-ai/client": "^1.2.3", 15 | "dotenv": "^16.4.7", 16 | "motia": "^0.1.0-beta.15", 17 | "openai": "^4.83.0", 18 | "react": "^19.0.0", 19 | "zod": "^3.24.1" 20 | }, 21 | "devDependencies": { 22 | "@types/react": "^18.3.18", 23 | "ts-node": "^10.9.2", 24 | "typescript": "^5.7.3" 25 | } 26 | } -------------------------------------------------------------------------------- /examples/vision-example/requirements.txt: -------------------------------------------------------------------------------- 1 | anthropic==0.31.2 2 | vision-agent==0.2.222 3 | ultraimport==0.0.7 -------------------------------------------------------------------------------- /examples/vision-example/steps/__init__.py: -------------------------------------------------------------------------------- 1 | import os, sys; sys.path.append(os.path.dirname(os.path.realpath(__file__))) -------------------------------------------------------------------------------- /examples/vision-example/steps/api.step.ts: -------------------------------------------------------------------------------- 1 | import { ApiRouteConfig, StepHandler } from 'motia' 2 | import { z } from 'zod' 3 | 4 | const bodySchema = z.object({ 5 | prompt: z.string(), 6 | }) 7 | 8 | export const config: ApiRouteConfig = { 9 | type: 'api', 10 | name: 'generate image api trigger', 11 | description: 'generate an ai image given a prompt', 12 | path: '/generate-image', 13 | method: 'POST', 14 | emits: ['enhance-image-prompt'], 15 | bodySchema: bodySchema, 16 | flows: ['generate-image'], 17 | } 18 | 19 | export const handler: StepHandler = async (req, { logger, emit }) => { 20 | logger.info('initialized generate image flow') 21 | 22 | await emit({ 23 | type: 'enhance-image-prompt', 24 | data: { 25 | prompt: req.body.prompt, 26 | }, 27 | }) 28 | 29 | return { 30 | status: 200, 31 | body: { message: `generate image flow initialized` }, 32 | } 33 | } -------------------------------------------------------------------------------- /examples/vision-example/steps/download_image.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | def download_image(image_url, save_path="image.png"): 4 | """Encodes an image from a URL to base64.""" 5 | 6 | response = requests.get(image_url) 7 | if response.status_code == 200: 8 | # Save the image locally 9 | with open(save_path, "wb") as f: 10 | f.write(response.content) 11 | 12 | return save_path 13 | else: 14 | return None -------------------------------------------------------------------------------- /examples/vision-example/steps/enhance_image_prompt.step.py: -------------------------------------------------------------------------------- 1 | from anthropic import Anthropic 2 | import os 3 | 4 | client = Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY")) 5 | 6 | config = { 7 | "type": "event", 8 | "name": "enhance image prompt", 9 | "description": "enhance a given image prompt", 10 | "subscribes": ["enhance-image-prompt"], 11 | "emits": ["generate-image"], 12 | "flows": ["generate-image"], 13 | "input": None, # No schema validation in Python 14 | } 15 | 16 | async def handler(args, ctx): 17 | ctx.logger.info('enhance image prompt', args) 18 | 19 | prompt = args.prompt 20 | 21 | prompt_enhancement_prompt = f""" 22 | You are a helpful assistant that can enhance a given image prompt. 23 | The original prompt is: {prompt} 24 | Please enhance the prompt to make it more specific and detailed. 25 | Include artistic details and style to the prompt to make the image more creative and unique. 26 | Make sure the prompt is not too long. Only return the enhanced prompt, no other text. 27 | """ 28 | 29 | response = client.messages.create( 30 | model="claude-3-sonnet-20240229", 31 | messages=[{ 32 | "role": "user", 33 | "content": prompt_enhancement_prompt 34 | }], 35 | max_tokens=1000 36 | ) 37 | 38 | enhanced_prompt = response.content[0].text 39 | 40 | ctx.logger.info('enhanced prompt', enhanced_prompt) 41 | 42 | await ctx.emit({ 43 | "type": 'generate-image', 44 | "data": {"prompt": enhanced_prompt, "original_prompt": prompt }, 45 | }) -------------------------------------------------------------------------------- /examples/vision-example/steps/eval-agent/eval-agent-results.api.step.ts: -------------------------------------------------------------------------------- 1 | import { ApiRouteConfig, StepHandler } from 'motia' 2 | import { z } from 'zod' 3 | import fs from 'fs' 4 | 5 | const bodySchema = z.object({}) 6 | 7 | export const config: ApiRouteConfig = { 8 | type: 'api', 9 | name: 'evaluate image generation flow results', 10 | description: 'initialize the evaluation agent, generate a new evaluation report from a dataset of image generation reports (created by the generate-image flow)', 11 | path: '/evaluate-image-generation-dataset', 12 | method: 'POST', 13 | emits: ['eval-image-generation-dataset'], 14 | bodySchema: bodySchema, 15 | flows: ['eval-agent'], 16 | } 17 | 18 | export const handler: StepHandler = async (req, { logger, emit }) => { 19 | logger.info('evaluate agent results') 20 | 21 | // Check for minimum number of report files 22 | const reportFiles = fs.readdirSync('tmp') 23 | .filter(file => file.endsWith('_report.txt')) 24 | 25 | if (reportFiles.length < 10) { 26 | return { 27 | status: 400, 28 | body: {message:`Insufficient number of report files. Found ${reportFiles.length}, but need at least 10 reports for an evaluation. Please run the generate-image flow first.`} 29 | } 30 | } 31 | 32 | await emit({ 33 | type: 'eval-image-generation-dataset', 34 | data: {}, 35 | }) 36 | 37 | return { 38 | status: 200, 39 | body: { message: `evaluate image generation flow results` }, 40 | } 41 | } -------------------------------------------------------------------------------- /examples/vision-example/tmp/1146231c-50ff-4351-ade2-ce93c657f9c1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MotiaDev/motia-examples/c1ea1854d55a9d75218e6c00a8302dcb98289bb8/examples/vision-example/tmp/1146231c-50ff-4351-ade2-ce93c657f9c1.png -------------------------------------------------------------------------------- /examples/vision-example/tmp/1146231c-50ff-4351-ade2-ce93c657f9c1_report.txt: -------------------------------------------------------------------------------- 1 | { 2 | "original_prompt": "create an image of a couple backpacking through a trail in the easter sierras. use a black and white image style. sketch style.", 3 | "prompt": "Create a stunning black and white sketch of a rugged couple trekking along a winding trail in the majestic Eastern Sierras. Capture the awe-inspiring mountain peaks towering above, their jagged silhouettes contrasting with the sun-dappled trees and rocky terrain. The couple should be rendered in a bold, expressive style, their backpacks and hiking gear accentuated with intricate hatching and crosshatching. Incorporate dynamic perspective, with the trail leading the eye towards the distant, hazy mountains. Use dramatic lighting and textured shading to convey the sense of adventure and wilderness.", 4 | "score": 85.0, 5 | "image_path": "/Users/rodrigomorales/Projects/motia/motia-examples/examples/vision-example/tmp/1146231c-50ff-4351-ade2-ce93c657f9c1.png" 6 | } 7 | -------------------------------------------------------------------------------- /examples/vision-example/tmp/2187137d-92cb-4f92-acf3-3bf669c1ac37.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MotiaDev/motia-examples/c1ea1854d55a9d75218e6c00a8302dcb98289bb8/examples/vision-example/tmp/2187137d-92cb-4f92-acf3-3bf669c1ac37.png -------------------------------------------------------------------------------- /examples/vision-example/tmp/2187137d-92cb-4f92-acf3-3bf669c1ac37_report.txt: -------------------------------------------------------------------------------- 1 | { 2 | "original_prompt": "create an image of a couple backpacking through a trail in the easter sierras. use a black and white image style. sketch style.", 3 | "prompt": "Render a mesmerizing sketch depicting a young adventurous couple traversing a winding trail in the majestic Eastern Sierra, their backpacks casting elongated shadows against the rugged mountain backdrop. Capture the scene in a striking black and white crosshatched style, with intricate linework conveying the textures of the granite peaks and swaying pines. Infuse the composition with a sense of awe and wonder, as if each step unveils nature's grandeur.", 4 | "score": 95.0, 5 | "image_path": "/Users/rodrigomorales/Projects/motia/motia-examples/examples/vision-example/tmp/2187137d-92cb-4f92-acf3-3bf669c1ac37.png" 6 | } 7 | -------------------------------------------------------------------------------- /examples/vision-example/tmp/50bf3c53-83f2-40c7-b3fc-923c7122740e.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MotiaDev/motia-examples/c1ea1854d55a9d75218e6c00a8302dcb98289bb8/examples/vision-example/tmp/50bf3c53-83f2-40c7-b3fc-923c7122740e.png -------------------------------------------------------------------------------- /examples/vision-example/tmp/50bf3c53-83f2-40c7-b3fc-923c7122740e_report.txt: -------------------------------------------------------------------------------- 1 | { 2 | "original_prompt": "create an image of a couple backpacking through a trail in the easter sierras. use a black and white image style. sketch style.", 3 | "prompt": "Sketch an evocative black-and-white scene of a young adventurous couple trekking along a winding trail in the majestic Eastern Sierra Nevada Mountains. Render their backpacks and hiking gear in intricate detail, accentuating the rugged textures. Depict the breathtaking mountain vistas with dramatic light and shadow, capturing the awe-inspiring grandeur of nature's majesty. Incorporate subtle romantic undertones through their body language and expressions, hinting at the shared sense of wonder and connection in the great outdoors.", 4 | "score": 100, 5 | "image_path": "/Users/rodrigomorales/Projects/motia/motia-examples/examples/vision-example/tmp/50bf3c53-83f2-40c7-b3fc-923c7122740e.png" 6 | } 7 | -------------------------------------------------------------------------------- /examples/vision-example/tmp/55ca6aaa-0c0a-490b-808d-28333d9c0358.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MotiaDev/motia-examples/c1ea1854d55a9d75218e6c00a8302dcb98289bb8/examples/vision-example/tmp/55ca6aaa-0c0a-490b-808d-28333d9c0358.png -------------------------------------------------------------------------------- /examples/vision-example/tmp/55ca6aaa-0c0a-490b-808d-28333d9c0358_report.txt: -------------------------------------------------------------------------------- 1 | { 2 | "original_prompt": "create an image of a couple backpacking through a trail in the easter sierras. use a black and white image style. sketch style.", 3 | "prompt": "Create a black and white sketch-style image depicting a young couple backpacking along a winding trail in the Eastern Sierra Nevada mountains. Render the scene with bold, expressive brushstrokes that capture the rugged grandeur of the towering peaks, the dappled light filtering through the pine forests, and the intrepid hikers silhouetted against the majestic landscape. Incorporate abstract elements, such as swirling clouds of ink or gestural marks, to convey the sense of adventure and the awe-inspiring natural beauty.", 4 | "score": 100, 5 | "image_path": "/Users/rodrigomorales/Projects/motia/motia-examples/examples/vision-example/tmp/55ca6aaa-0c0a-490b-808d-28333d9c0358.png" 6 | } 7 | -------------------------------------------------------------------------------- /examples/vision-example/tmp/6aacba7e-2649-4a99-a0da-3afa2915d0f4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MotiaDev/motia-examples/c1ea1854d55a9d75218e6c00a8302dcb98289bb8/examples/vision-example/tmp/6aacba7e-2649-4a99-a0da-3afa2915d0f4.png -------------------------------------------------------------------------------- /examples/vision-example/tmp/6aacba7e-2649-4a99-a0da-3afa2915d0f4_report.txt: -------------------------------------------------------------------------------- 1 | { 2 | "original_prompt": "create an image of a couple backpacking through a trail in the easter sierras. use a black and white image style. sketch style.", 3 | "prompt": "Render a monochrome sketch portraying an adventurous couple trekking along a winding trail amidst the majestic Eastern Sierra peaks, their silhouettes in stark contrast with the rugged landscape. Capture the essence of their journey through intricate crosshatching and delicate stippling, evoking a sense of wilderness and exploration. Incorporate elements of chiaroscuro to accentuate the dramatic interplay of light and shadow, casting a warm, hazy glow over the scene.", 4 | "score": 90.0, 5 | "image_path": "/Users/rodrigomorales/Projects/motia/motia-examples/examples/vision-example/tmp/6aacba7e-2649-4a99-a0da-3afa2915d0f4.png" 6 | } 7 | -------------------------------------------------------------------------------- /examples/vision-example/tmp/6df32658-17f8-4d2b-8129-00677a1906b3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MotiaDev/motia-examples/c1ea1854d55a9d75218e6c00a8302dcb98289bb8/examples/vision-example/tmp/6df32658-17f8-4d2b-8129-00677a1906b3.png -------------------------------------------------------------------------------- /examples/vision-example/tmp/6df32658-17f8-4d2b-8129-00677a1906b3_report.txt: -------------------------------------------------------------------------------- 1 | { 2 | "original_prompt": "create an image of a couple backpacking through a trail in the easter sierras. use a black and white image style. sketch style.", 3 | "prompt": "Render a monochromatic sketch depicting a couple navigating a winding trail in the majestic Eastern Sierra Nevada mountains. Capture the rugged beauty of the landscape with jagged peaks, towering pines, and wispy clouds. Employ dynamic lines and crosshatching to convey texture and depth. Portray the couple's sense of adventure through their posture, hiking gear, and the vast wilderness stretching before them.", 4 | "score": 95.0, 5 | "image_path": "/Users/rodrigomorales/Projects/motia/motia-examples/examples/vision-example/tmp/6df32658-17f8-4d2b-8129-00677a1906b3.png" 6 | } 7 | -------------------------------------------------------------------------------- /examples/vision-example/tmp/89205ac8-200a-45a9-b27e-5768bf38a12c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MotiaDev/motia-examples/c1ea1854d55a9d75218e6c00a8302dcb98289bb8/examples/vision-example/tmp/89205ac8-200a-45a9-b27e-5768bf38a12c.png -------------------------------------------------------------------------------- /examples/vision-example/tmp/89205ac8-200a-45a9-b27e-5768bf38a12c_report.txt: -------------------------------------------------------------------------------- 1 | { 2 | "original_prompt": "create an image of a couple backpacking through a trail in the easter sierras. use a black and white image style. sketch style.", 3 | "prompt": "Render a captivating sketch in charcoal tones, depicting an adventurous couple traversing a winding trail amidst the majestic Eastern Sierra peaks. Capture the rugged terrain with bold, textured strokes, contrasted by delicate cross-hatching for intricate details. Imbue the scene with a sense of wonder, their silhouettes dwarfed by towering pine forests and jagged rock formations, highlighting the awe-inspiring grandeur of nature's canvas.", 4 | "score": 95.0, 5 | "image_path": "/Users/rodrigomorales/Projects/motia/motia-examples/examples/vision-example/tmp/89205ac8-200a-45a9-b27e-5768bf38a12c.png" 6 | } 7 | -------------------------------------------------------------------------------- /examples/vision-example/tmp/b279cdc4-ffd3-452d-9f7e-06cfc0c5a0fb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MotiaDev/motia-examples/c1ea1854d55a9d75218e6c00a8302dcb98289bb8/examples/vision-example/tmp/b279cdc4-ffd3-452d-9f7e-06cfc0c5a0fb.png -------------------------------------------------------------------------------- /examples/vision-example/tmp/b279cdc4-ffd3-452d-9f7e-06cfc0c5a0fb_report.txt: -------------------------------------------------------------------------------- 1 | { 2 | "original_prompt": "create an image of a couple backpacking through a trail in the easter sierras. use a black and white image style. sketch style.", 3 | "prompt": "Create a black and white sketch-style image of a young adventurous couple hiking along a winding trail through the majestic Eastern Sierra mountains. Render the scene with bold, expressive brushstrokes, capturing the rugged peaks towering above and dappled sunlight filtering through evergreen trees. Compose the image from a low vantage point, emphasizing the couple's small stature amidst the vast, awe-inspiring natural landscape.", 4 | "score": 100, 5 | "image_path": "/Users/rodrigomorales/Projects/motia/motia-examples/examples/vision-example/tmp/b279cdc4-ffd3-452d-9f7e-06cfc0c5a0fb.png" 6 | } 7 | -------------------------------------------------------------------------------- /examples/vision-example/tmp/dc8360ff-ad1d-4648-97db-f974606bad62.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MotiaDev/motia-examples/c1ea1854d55a9d75218e6c00a8302dcb98289bb8/examples/vision-example/tmp/dc8360ff-ad1d-4648-97db-f974606bad62.png -------------------------------------------------------------------------------- /examples/vision-example/tmp/dc8360ff-ad1d-4648-97db-f974606bad62_report.txt: -------------------------------------------------------------------------------- 1 | { 2 | "original_prompt": "create an image of a couple backpacking through a trail in the easter sierras. use a black and white image style. sketch style.", 3 | "prompt": "Render a sketch-style black and white image of a young adventurous couple trekking along a rugged trail amidst towering peaks and jagged rock formations of the eastern Sierra Nevada range. Capture the scene with dynamic linework, bold contrasts, and a sense of scale and grandeur. Convey the couple's camaraderie and enthusiasm for the outdoors through their body language and expressions. Incorporate intricate textures and patterns to depict the terrain's ruggedness and the couple's well-worn hiking gear.", 4 | "score": 95.0, 5 | "image_path": "/Users/rodrigomorales/Projects/motia/motia-examples/examples/vision-example/tmp/dc8360ff-ad1d-4648-97db-f974606bad62.png" 6 | } 7 | -------------------------------------------------------------------------------- /examples/vision-example/tmp/dd3e2dce-6b1e-4de9-9024-7ca60e59ef17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MotiaDev/motia-examples/c1ea1854d55a9d75218e6c00a8302dcb98289bb8/examples/vision-example/tmp/dd3e2dce-6b1e-4de9-9024-7ca60e59ef17.png -------------------------------------------------------------------------------- /examples/vision-example/tmp/dd3e2dce-6b1e-4de9-9024-7ca60e59ef17_report.txt: -------------------------------------------------------------------------------- 1 | { 2 | "original_prompt": "create an image of a couple backpacking through a trail in the easter sierras. use a black and white image style. sketch style.", 3 | "prompt": "Create a sketch-style black and white image of a young adventurous couple backpacking through a winding trail in the majestic Eastern Sierra Nevada mountains, surrounded by towering jagged peaks, cascading waterfalls, and fields of wildflowers, captured in a dramatic chiaroscuro lighting with bold contrasts and expressive brush strokes.", 4 | "score": 90.0, 5 | "image_path": "/Users/rodrigomorales/Projects/motia/motia-examples/examples/vision-example/tmp/dd3e2dce-6b1e-4de9-9024-7ca60e59ef17.png" 6 | } 7 | -------------------------------------------------------------------------------- /examples/vision-example/tmp/f4b768a8-08a8-4ec8-8377-3d5ae3cdc31f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MotiaDev/motia-examples/c1ea1854d55a9d75218e6c00a8302dcb98289bb8/examples/vision-example/tmp/f4b768a8-08a8-4ec8-8377-3d5ae3cdc31f.png -------------------------------------------------------------------------------- /examples/vision-example/tmp/f4b768a8-08a8-4ec8-8377-3d5ae3cdc31f_report.txt: -------------------------------------------------------------------------------- 1 | { 2 | "original_prompt": "create an image of a couple backpacking through a trail in the easter sierras. use a black and white image style. sketch style.", 3 | "prompt": "Create a black and white sketch-style image of a young adventurous couple hiking through a winding trail in the majestic Eastern Sierra mountains. Capture the rugged, snow-capped peaks and pine forests in the distance, with rays of sunlight piercing through the clouds. Depict the couple carrying backpacks, trekking poles, and a sense of wonder on their faces as they traverse the breathtaking wilderness landscape.", 4 | "score": 95.0, 5 | "image_path": "/Users/rodrigomorales/Projects/motia/motia-examples/examples/vision-example/tmp/f4b768a8-08a8-4ec8-8377-3d5ae3cdc31f.png" 6 | } 7 | -------------------------------------------------------------------------------- /examples/vision-example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "resolveJsonModule": true, 11 | "allowJs": true, 12 | "outDir": "dist", 13 | "rootDir": ".", 14 | "baseUrl": ".", 15 | "jsx": "react-jsx" 16 | }, 17 | "include": [ 18 | "**/*.ts", 19 | "**/*.tsx", 20 | "**/*.js", 21 | "**/*.jsx" 22 | ], 23 | "exclude": [ 24 | "node_modules", 25 | "dist", 26 | "tests" 27 | ] 28 | } --------------------------------------------------------------------------------