motia 0.6.0-beta.120 → 0.6.0-beta.122

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/dist/cjs/cli.js +0 -8
  2. package/dist/cjs/create/index.js +1 -21
  3. package/dist/cjs/create/interactive.js +1 -1
  4. package/dist/cjs/create/templates/generate.js +11 -8
  5. package/dist/cjs/create/templates/generate.ts +10 -10
  6. package/dist/cjs/create/templates/index.js +1 -2
  7. package/dist/cjs/create/templates/index.ts +1 -2
  8. package/dist/cjs/create/templates/python/motia-workbench.json +14 -16
  9. package/dist/cjs/create/templates/python/requirements.txt +2 -0
  10. package/dist/cjs/create/templates/python/steps/api_step.py-features.json.txt +69 -0
  11. package/dist/cjs/create/templates/python/steps/api_step.py.txt +52 -0
  12. package/dist/cjs/create/templates/python/steps/notification_step.py.txt +40 -0
  13. package/dist/cjs/create/templates/python/steps/process_food_order_step.py-features.json.txt +68 -0
  14. package/dist/cjs/create/templates/python/steps/process_food_order_step.py.txt +50 -0
  15. package/dist/cjs/create/templates/python/steps/services/__init__.py.txt +0 -0
  16. package/dist/cjs/create/templates/python/steps/services/pet_store.py.txt +37 -0
  17. package/dist/cjs/create/templates/python/steps/services/types.py.txt +19 -0
  18. package/dist/cjs/create/templates/python/steps/state_audit_cron_step.py-features.json.txt +26 -0
  19. package/dist/cjs/create/templates/python/steps/state_audit_cron_step.py.txt +39 -0
  20. package/dist/cjs/create/templates/python/tutorial.tsx.txt +647 -0
  21. package/dist/cjs/create/templates/typescript/motia-workbench.json +28 -0
  22. package/dist/cjs/create/templates/typescript/services/pet-store.ts.txt +29 -0
  23. package/dist/cjs/create/templates/typescript/services/types.ts.txt +21 -0
  24. package/dist/cjs/create/templates/typescript/steps/api.step.ts-features.json.txt +67 -0
  25. package/dist/cjs/create/templates/{basic-tutorial/01-api.step.ts.txt → typescript/steps/api.step.ts.txt} +6 -14
  26. package/dist/cjs/create/templates/typescript/steps/notification.step.ts.txt +35 -0
  27. package/dist/cjs/create/templates/typescript/steps/process-food-order.step.ts-features.json.txt +67 -0
  28. package/dist/{esm/create/templates/basic-tutorial/02-process-food-order.step.ts.txt → cjs/create/templates/typescript/steps/process-food-order.step.ts.txt} +21 -7
  29. package/dist/cjs/create/templates/typescript/steps/state-audit-cron.step.ts-features.json.txt +26 -0
  30. package/dist/cjs/create/templates/typescript/steps/state-audit-cron.step.ts.txt +51 -0
  31. package/dist/cjs/create/templates/typescript/tutorial.tsx.txt +647 -0
  32. package/dist/esm/cli.js +0 -8
  33. package/dist/esm/create/index.js +1 -21
  34. package/dist/esm/create/interactive.js +1 -1
  35. package/dist/esm/create/templates/generate.js +11 -8
  36. package/dist/esm/create/templates/generate.ts +10 -10
  37. package/dist/esm/create/templates/index.js +1 -2
  38. package/dist/esm/create/templates/index.ts +1 -2
  39. package/dist/esm/create/templates/python/motia-workbench.json +14 -16
  40. package/dist/esm/create/templates/python/requirements.txt +2 -0
  41. package/dist/esm/create/templates/python/steps/api_step.py-features.json.txt +69 -0
  42. package/dist/esm/create/templates/python/steps/api_step.py.txt +52 -0
  43. package/dist/esm/create/templates/python/steps/notification_step.py.txt +40 -0
  44. package/dist/esm/create/templates/python/steps/process_food_order_step.py-features.json.txt +68 -0
  45. package/dist/esm/create/templates/python/steps/process_food_order_step.py.txt +50 -0
  46. package/dist/esm/create/templates/python/steps/services/__init__.py.txt +0 -0
  47. package/dist/esm/create/templates/python/steps/services/pet_store.py.txt +37 -0
  48. package/dist/esm/create/templates/python/steps/services/types.py.txt +19 -0
  49. package/dist/esm/create/templates/python/steps/state_audit_cron_step.py-features.json.txt +26 -0
  50. package/dist/esm/create/templates/python/steps/state_audit_cron_step.py.txt +39 -0
  51. package/dist/esm/create/templates/python/tutorial.tsx.txt +647 -0
  52. package/dist/esm/create/templates/typescript/motia-workbench.json +28 -0
  53. package/dist/esm/create/templates/typescript/services/pet-store.ts.txt +29 -0
  54. package/dist/esm/create/templates/typescript/services/types.ts.txt +21 -0
  55. package/dist/esm/create/templates/typescript/steps/api.step.ts-features.json.txt +67 -0
  56. package/dist/esm/create/templates/{basic-tutorial/01-api.step.ts.txt → typescript/steps/api.step.ts.txt} +6 -14
  57. package/dist/esm/create/templates/typescript/steps/notification.step.ts.txt +35 -0
  58. package/dist/esm/create/templates/typescript/steps/process-food-order.step.ts-features.json.txt +67 -0
  59. package/dist/{cjs/create/templates/basic-tutorial/02-process-food-order.step.ts.txt → esm/create/templates/typescript/steps/process-food-order.step.ts.txt} +21 -7
  60. package/dist/esm/create/templates/typescript/steps/state-audit-cron.step.ts-features.json.txt +26 -0
  61. package/dist/esm/create/templates/typescript/steps/state-audit-cron.step.ts.txt +51 -0
  62. package/dist/esm/create/templates/typescript/tutorial.tsx.txt +647 -0
  63. package/package.json +4 -4
  64. package/dist/cjs/create/setup-tutorial-flow.d.ts +0 -6
  65. package/dist/cjs/create/setup-tutorial-flow.js +0 -30
  66. package/dist/cjs/create/templates/basic-tutorial/03-state-audit-cron.step.ts.txt +0 -42
  67. package/dist/cjs/create/templates/basic-tutorial/04_new_order_notifications.step.py.txt +0 -27
  68. package/dist/cjs/create/templates/basic-tutorial/motia-workbench.json +0 -28
  69. package/dist/cjs/create/templates/basic-tutorial/services/pet-store.ts.txt +0 -32
  70. package/dist/cjs/create/templates/default/00-noop.step.ts.txt +0 -33
  71. package/dist/cjs/create/templates/default/00-noop.step.tsx.txt +0 -18
  72. package/dist/cjs/create/templates/default/01-api.step.ts.txt +0 -70
  73. package/dist/cjs/create/templates/default/02-test-state.step.ts.txt +0 -53
  74. package/dist/cjs/create/templates/default/03-check-state-change.step.ts.txt +0 -54
  75. package/dist/cjs/create/templates/default/motia-workbench.json +0 -29
  76. package/dist/cjs/create/templates/python/00_noop_step.py.txt +0 -24
  77. package/dist/cjs/create/templates/python/00_noop_step.tsx.txt +0 -18
  78. package/dist/cjs/create/templates/python/01_api_step.py.txt +0 -53
  79. package/dist/cjs/create/templates/python/02_test_state_step.py.txt +0 -38
  80. package/dist/cjs/create/templates/python/03_check_state_change_step.py.txt +0 -43
  81. package/dist/esm/create/setup-tutorial-flow.d.ts +0 -6
  82. package/dist/esm/create/setup-tutorial-flow.js +0 -23
  83. package/dist/esm/create/templates/basic-tutorial/03-state-audit-cron.step.ts.txt +0 -42
  84. package/dist/esm/create/templates/basic-tutorial/04_new_order_notifications.step.py.txt +0 -27
  85. package/dist/esm/create/templates/basic-tutorial/motia-workbench.json +0 -28
  86. package/dist/esm/create/templates/basic-tutorial/services/pet-store.ts.txt +0 -32
  87. package/dist/esm/create/templates/default/00-noop.step.ts.txt +0 -33
  88. package/dist/esm/create/templates/default/00-noop.step.tsx.txt +0 -18
  89. package/dist/esm/create/templates/default/01-api.step.ts.txt +0 -70
  90. package/dist/esm/create/templates/default/02-test-state.step.ts.txt +0 -53
  91. package/dist/esm/create/templates/default/03-check-state-change.step.ts.txt +0 -54
  92. package/dist/esm/create/templates/default/motia-workbench.json +0 -29
  93. package/dist/esm/create/templates/python/00_noop_step.py.txt +0 -24
  94. package/dist/esm/create/templates/python/00_noop_step.tsx.txt +0 -18
  95. package/dist/esm/create/templates/python/01_api_step.py.txt +0 -53
  96. package/dist/esm/create/templates/python/02_test_state_step.py.txt +0 -38
  97. package/dist/esm/create/templates/python/03_check_state_change_step.py.txt +0 -43
  98. package/dist/types/create/setup-tutorial-flow.d.ts +0 -6
package/dist/cjs/cli.js CHANGED
@@ -156,14 +156,6 @@ generate
156
156
  stepFilePath: arg.dir,
157
157
  });
158
158
  });
159
- generate
160
- .command('tutorial-flow')
161
- .description('Download the tutorial flow into an existing motia project')
162
- .action((0, config_utils_1.handler)(async (_, context) => {
163
- const { createTutorialFlow } = require('./create/setup-tutorial-flow');
164
- await createTutorialFlow({ context });
165
- process.exit(0);
166
- }));
167
159
  const docker = commander_1.program.command('docker').description('Motia docker commands');
168
160
  docker
169
161
  .command('setup')
@@ -177,31 +177,11 @@ const create = async ({ projectName, template, cursorEnabled, context }) => {
177
177
  fs_1.default.cpSync(cursorTemplateDir, cursorTargetDir, { recursive: true });
178
178
  context.log('cursor-folder-created', (message) => message.tag('success').append('Folder').append('.cursor', 'cyan').append('has been created.'));
179
179
  }
180
- const stepsDir = path_1.default.join(rootDir, 'steps');
181
- if (!(0, utils_1.checkIfDirectoryExists)(stepsDir)) {
182
- fs_1.default.mkdirSync(stepsDir);
183
- context.log('steps-directory-created', (message) => message.tag('success').append('Folder').append('steps', 'cyan').append('has been created.'));
184
- }
185
- if (!(0, utils_1.checkIfDirectoryExists)(path_1.default.join(stepsDir, 'basic-tutorial'))) {
186
- fs_1.default.mkdirSync(path_1.default.join(stepsDir, 'basic-tutorial'));
187
- }
188
- await (0, setup_template_1.setupTemplate)('basic-tutorial', stepsDir, context);
189
180
  if (template) {
190
- if (!(0, utils_1.checkIfDirectoryExists)(path_1.default.join(stepsDir, template))) {
191
- fs_1.default.mkdirSync(path_1.default.join(stepsDir, template));
192
- }
193
- await (0, setup_template_1.setupTemplate)(template, stepsDir, context);
181
+ await (0, setup_template_1.setupTemplate)(template, rootDir, context);
194
182
  }
195
183
  const packageManager = await installNodeDependencies(rootDir, context);
196
184
  if (template === 'python') {
197
- if (!(0, utils_1.checkIfFileExists)(rootDir, 'requirements.txt')) {
198
- const requirementsContent = [
199
- // TODO: motia PyPi package
200
- // Add other Python dependencies as needed
201
- ].join('\n');
202
- fs_1.default.writeFileSync(path_1.default.join(rootDir, 'requirements.txt'), requirementsContent);
203
- context.log('requirements-txt-created', (message) => message.tag('success').append('File').append('requirements.txt', 'gray').append('has been created.'));
204
- }
205
185
  await (0, install_1.pythonInstall)({ baseDir: rootDir });
206
186
  }
207
187
  await (0, generate_types_1.generateTypes)(rootDir);
@@ -8,7 +8,7 @@ const inquirer_1 = __importDefault(require("inquirer"));
8
8
  const colors_1 = __importDefault(require("colors"));
9
9
  const index_1 = require("./index");
10
10
  const choices = {
11
- default: 'Base (TypeScript)',
11
+ typescript: 'Base (TypeScript)',
12
12
  python: 'Base (Python)',
13
13
  };
14
14
  const createInteractive = async (_args, context) => {
@@ -44,20 +44,23 @@ const generateTemplateSteps = (templateFolder) => {
44
44
  try {
45
45
  for (const fileName of files) {
46
46
  const filePath = path.join(templatePath, fileName);
47
- if ((0, fs_1.statSync)(filePath).isDirectory() && !filePath.match(/services|utils|lib/)) {
48
- // ignore folders
49
- continue;
47
+ const targetFilePath = path.join(rootDir, fileName);
48
+ const targetDir = path.dirname(targetFilePath);
49
+ try {
50
+ // Check if it's a directory in the template
51
+ (0, fs_1.statSync)(targetDir);
52
+ }
53
+ catch {
54
+ (0, fs_1.mkdirSync)(targetDir, { recursive: true });
50
55
  }
51
56
  if ((0, fs_1.statSync)(filePath).isDirectory()) {
52
57
  const folderPath = path.basename(filePath);
53
- (0, fs_1.mkdirSync)(path.join(rootDir, templateFolder, folderPath));
58
+ (0, fs_1.mkdirSync)(path.join(rootDir, folderPath), { recursive: true });
54
59
  continue;
55
60
  }
56
- const sanitizedFileName = fileName.replace('.txt', '');
61
+ const sanitizedFileName = fileName === 'requirements.txt' ? fileName : fileName.replace('.txt', '');
57
62
  const isWorkbenchConfig = fileName.match('motia-workbench.json');
58
- const generateFilePath = path.join(...(isWorkbenchConfig
59
- ? [rootDir.match(/steps/) ? path.join(rootDir, '..') : rootDir, sanitizedFileName]
60
- : [rootDir, templateFolder, sanitizedFileName]));
63
+ const generateFilePath = path.join(rootDir, sanitizedFileName);
61
64
  let content = await fs_1.promises.readFile(filePath, 'utf8');
62
65
  // Make sure statSync doesn't break the execution if the file doesn't exist
63
66
  try {
@@ -13,25 +13,25 @@ export const generateTemplateSteps = (templateFolder: string): Generator => {
13
13
  try {
14
14
  for (const fileName of files) {
15
15
  const filePath = path.join(templatePath, fileName)
16
+ const targetFilePath = path.join(rootDir, fileName)
17
+ const targetDir = path.dirname(targetFilePath)
16
18
 
17
- if (statSync(filePath).isDirectory() && !filePath.match(/services|utils|lib/)) {
18
- // ignore folders
19
- continue
19
+ try {
20
+ // Check if it's a directory in the template
21
+ statSync(targetDir)
22
+ } catch {
23
+ mkdirSync(targetDir, { recursive: true })
20
24
  }
21
25
 
22
26
  if (statSync(filePath).isDirectory()) {
23
27
  const folderPath = path.basename(filePath)
24
- mkdirSync(path.join(rootDir, templateFolder, folderPath))
28
+ mkdirSync(path.join(rootDir, folderPath), { recursive: true })
25
29
  continue
26
30
  }
27
31
 
28
- const sanitizedFileName = fileName.replace('.txt', '')
32
+ const sanitizedFileName = fileName === 'requirements.txt' ? fileName : fileName.replace('.txt', '')
29
33
  const isWorkbenchConfig = fileName.match('motia-workbench.json')
30
- const generateFilePath = path.join(
31
- ...(isWorkbenchConfig
32
- ? [rootDir.match(/steps/) ? path.join(rootDir, '..') : rootDir, sanitizedFileName]
33
- : [rootDir, templateFolder, sanitizedFileName]),
34
- )
34
+ const generateFilePath = path.join(rootDir, sanitizedFileName)
35
35
  let content = await fs.readFile(filePath, 'utf8')
36
36
 
37
37
  // Make sure statSync doesn't break the execution if the file doesn't exist
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.templates = void 0;
4
4
  const generate_1 = require("./generate");
5
5
  exports.templates = {
6
- default: (0, generate_1.generateTemplateSteps)('default'),
6
+ typescript: (0, generate_1.generateTemplateSteps)('typescript'),
7
7
  python: (0, generate_1.generateTemplateSteps)('python'),
8
- 'basic-tutorial': (0, generate_1.generateTemplateSteps)('basic-tutorial'),
9
8
  };
@@ -1,7 +1,6 @@
1
1
  import { generateTemplateSteps, Generator } from './generate'
2
2
 
3
3
  export const templates: Record<string, Generator> = {
4
- default: generateTemplateSteps('default'),
4
+ typescript: generateTemplateSteps('typescript'),
5
5
  python: generateTemplateSteps('python'),
6
- 'basic-tutorial': generateTemplateSteps('basic-tutorial'),
7
6
  }
@@ -1,28 +1,26 @@
1
1
  [
2
2
  {
3
- "id": "default",
3
+ "id": "python-tutorial",
4
4
  "config": {
5
- "steps/00_noop_step.py": {
6
- "x": -507,
7
- "y": 111,
5
+ "steps/state_audit_cron_step.py": {
6
+ "x": -38,
7
+ "y": 683,
8
8
  "sourceHandlePosition": "right"
9
9
  },
10
- "steps/01_api_step.py": {
11
- "x": -281,
12
- "y": 94,
13
- "sourceHandlePosition": "right",
10
+ "steps/process_food_order_step.py": {
11
+ "x": 384,
12
+ "y": 476,
14
13
  "targetHandlePosition": "left"
15
14
  },
16
- "steps/02_test_state_step.py": {
17
- "x": 60,
18
- "y": 36,
19
- "sourceHandlePosition": "bottom",
15
+ "steps/notification_step.py": {
16
+ "x": 601,
17
+ "y": 724,
20
18
  "targetHandlePosition": "left"
21
19
  },
22
- "steps/03_check_state_change_step.py": {
23
- "x": 78,
24
- "y": 199,
25
- "targetHandlePosition": "top"
20
+ "steps/api_step.py": {
21
+ "x": 15,
22
+ "y": 461,
23
+ "sourceHandlePosition": "right"
26
24
  }
27
25
  }
28
26
  }
@@ -0,0 +1,2 @@
1
+ pydantic>=2.6.1
2
+ httpx>=0.28.1
@@ -0,0 +1,69 @@
1
+ [
2
+ {
3
+ "id": "step-configuration",
4
+ "title": "Step Configuration",
5
+ "description": "All steps should have a defined configuration, this is how you define the step's behavior and how it will be triggered.",
6
+ "lines": [
7
+ "6-30"
8
+ ]
9
+ },
10
+ {
11
+ "id": "api-configuration",
12
+ "title": "API Step",
13
+ "description": "Definition of an API endpoint",
14
+ "lines": [
15
+ "23-24"
16
+ ]
17
+ },
18
+ {
19
+ "id": "request-body",
20
+ "title": "Request body",
21
+ "description": "Definition of the expected request body. Motia will automatically generate types based on this schema.",
22
+ "lines": [
23
+ "6-16",
24
+ "25"
25
+ ]
26
+ },
27
+ {
28
+ "id": "response-payload",
29
+ "title": "Response Payload",
30
+ "description": "Definition of the expected response payload, Motia will generate the types automatically based on this schema. This is also important to create the Open API spec later.",
31
+ "lines": [
32
+ "4",
33
+ "26-28"
34
+ ]
35
+ },
36
+ {
37
+ "id": "event-driven-architecture",
38
+ "title": "Emits",
39
+ "description": "We can define the events that this step will emit, this is how we can trigger other Motia Steps.",
40
+ "lines": [
41
+ "29",
42
+ "42-50"
43
+ ]
44
+ },
45
+ {
46
+ "id": "handler",
47
+ "title": "Handler",
48
+ "description": "The handler is the function that will be executed when the step is triggered. This one receives the request body and emits events.",
49
+ "lines": [
50
+ "32-52"
51
+ ]
52
+ },
53
+ {
54
+ "id": "logger",
55
+ "title": "Logger",
56
+ "description": "The logger is a utility that allows you to log messages to the console. It is available in the handler function. We encourage you to use it instead of console.log. It will automatically be tied to the trace id of the request.",
57
+ "lines": [
58
+ "34"
59
+ ]
60
+ },
61
+ {
62
+ "id": "http-response",
63
+ "title": "HTTP Response",
64
+ "description": "The handler can return a response to the client. This is how we can return a response to the client. It must comply with the responseSchema defined in the step configuration.",
65
+ "lines": [
66
+ "52"
67
+ ]
68
+ }
69
+ ]
@@ -0,0 +1,52 @@
1
+ from pydantic import BaseModel
2
+ from typing import Optional
3
+ from .services.pet_store import pet_store_service
4
+ from .services.types import Pet
5
+
6
+ class PetRequest(BaseModel):
7
+ name: str
8
+ photo_url: str
9
+
10
+ class FoodOrder(BaseModel):
11
+ id: str
12
+ quantity: int
13
+
14
+ class RequestBody(BaseModel):
15
+ pet: PetRequest
16
+ food_order: Optional[FoodOrder] = None
17
+
18
+ config = {
19
+ "type": "api",
20
+ "name": "PythonApiTrigger",
21
+ "description": "basic-tutorial api trigger",
22
+ "flows": ["python-tutorial"],
23
+ "method": "POST",
24
+ "path": "/python-basic-tutorial",
25
+ "bodySchema": RequestBody.model_json_schema(),
26
+ "responseSchema": {
27
+ 200: Pet.model_json_schema(),
28
+ },
29
+ "emits": ["python-process-food-order"],
30
+ }
31
+
32
+ async def handler(req, context):
33
+ body = req.get("body", {})
34
+ context.logger.info("Step 01 – Processing API Step", {"body": body})
35
+
36
+ pet = body.get("pet", {})
37
+ food_order = body.get("food_order", {})
38
+
39
+ new_pet_record = await pet_store_service.create_pet(pet)
40
+
41
+ if food_order:
42
+ await context.emit({
43
+ "topic": "python-process-food-order",
44
+ "data": {
45
+ "id": food_order.get("id"),
46
+ "quantity": food_order.get("quantity"),
47
+ "email": "test@test.com", # sample email
48
+ "pet_id": new_pet_record.get("id"),
49
+ },
50
+ })
51
+
52
+ return {"status": 200, "body": new_pet_record}
@@ -0,0 +1,40 @@
1
+ from pydantic import BaseModel
2
+ from typing import Dict, Any
3
+ import re
4
+
5
+ class InputSchema(BaseModel):
6
+ template_id: str
7
+ email: str
8
+ template_data: Dict[str, Any]
9
+
10
+ config = {
11
+ "type": "event",
12
+ "name": "PythonNotification",
13
+ "description": "Checks a state change",
14
+ "flows": ["python-tutorial"],
15
+ "subscribes": ["python-notification"],
16
+ "emits": [],
17
+ "input": InputSchema.model_json_schema(),
18
+ }
19
+
20
+ async def handler(input_data, context):
21
+ email = input_data.get("email")
22
+ template_id = input_data.get("template_id")
23
+ template_data = input_data.get("template_data")
24
+
25
+ redacted_email = re.sub(r'(?<=.{2}).(?=.*@)', '*', email)
26
+
27
+ context.logger.info("Processing Notification", {
28
+ "template_id": template_id,
29
+ "template_data": template_data,
30
+ "email": redacted_email,
31
+ })
32
+
33
+ # This represents a call to some sort of
34
+ # notification service to indicate that a
35
+ # new order has been placed
36
+ context.logger.info("New notification sent", {
37
+ "template_id": template_id,
38
+ "email": redacted_email,
39
+ "template_data": template_data,
40
+ })
@@ -0,0 +1,68 @@
1
+ [
2
+ {
3
+ "id": "step-configuration",
4
+ "title": "Step Configuration",
5
+ "description": "All steps should have a defined configuration, this is how you define the step's behavior and how it will be triggered.",
6
+ "lines": [
7
+ "5-19"
8
+ ]
9
+ },
10
+ {
11
+ "id": "event-configuration",
12
+ "title": "Event Step",
13
+ "description": "Definition of an event step that subscribes to specific topics",
14
+ "lines": [
15
+ "12",
16
+ "15-16"
17
+ ]
18
+ },
19
+ {
20
+ "id": "input-schema",
21
+ "title": "Input Schema",
22
+ "description": "Definition of the expected input data structure from the subscribed topic. Motia will automatically generate types based on this schema.",
23
+ "lines": [
24
+ "5-9",
25
+ "17"
26
+ ]
27
+ },
28
+ {
29
+ "id": "event-emits",
30
+ "title": "Emits",
31
+ "description": "We can define the events that this step will emit, triggering other Motia Steps.",
32
+ "lines": [
33
+ "17"
34
+ ]
35
+ },
36
+ {
37
+ "id": "handler",
38
+ "title": "Handler",
39
+ "description": "The handler is the function that will be executed when the step receives an event from its subscribed topic. It processes the input data and can emit new events.",
40
+ "lines": [
41
+ "21-50"
42
+ ]
43
+ },
44
+ {
45
+ "id": "state",
46
+ "title": "State Management",
47
+ "description": "The handler demonstrates state management by storing order data that can be accessed by other steps.",
48
+ "lines": [
49
+ "35"
50
+ ]
51
+ },
52
+ {
53
+ "id": "event-emission",
54
+ "title": "Event Emission",
55
+ "description": "After processing the order, the handler emits a new event to notify other steps about the new order.",
56
+ "lines": [
57
+ "37-50"
58
+ ]
59
+ },
60
+ {
61
+ "id": "logger",
62
+ "title": "Logger",
63
+ "description": "The logger is a utility that allows you to log messages to the console. It is available in the handler function and automatically ties to the trace id of the request.",
64
+ "lines": [
65
+ "22"
66
+ ]
67
+ }
68
+ ]
@@ -0,0 +1,50 @@
1
+ from pydantic import BaseModel
2
+ from datetime import datetime
3
+ from .services.pet_store import pet_store_service
4
+
5
+ class InputSchema(BaseModel):
6
+ id: str
7
+ email: str
8
+ quantity: int
9
+ pet_id: int
10
+
11
+ config = {
12
+ "type": "event",
13
+ "name": "PythonProcessFoodOrder",
14
+ "description": "basic-tutorial event step, demonstrates how to consume an event from a topic and persist data in state",
15
+ "flows": ["python-tutorial"],
16
+ "subscribes": ["python-process-food-order"],
17
+ "emits": ["python-notification"],
18
+ "input": InputSchema.model_json_schema(),
19
+ }
20
+
21
+ async def handler(input_data, context):
22
+ context.logger.info("Step 02 – Process food order", {"input": input_data})
23
+
24
+ order = await pet_store_service.create_order({
25
+ "id": input_data.get("id"),
26
+ "quantity": input_data.get("quantity"),
27
+ "pet_id": input_data.get("pet_id"),
28
+ "email": input_data.get("email"),
29
+ "ship_date": datetime.now().isoformat(),
30
+ "status": "placed",
31
+ })
32
+
33
+ context.logger.info("Order created", {"order": order})
34
+
35
+ await context.state.set("orders_python", order.get("id"), order)
36
+
37
+ await context.emit({
38
+ "topic": "python-notification",
39
+ "data": {
40
+ "email": input_data["email"],
41
+ "template_id": "new-order",
42
+ "template_data": {
43
+ "status": order.get("status"),
44
+ "ship_date": order.get("shipDate"),
45
+ "id": order.get("id"),
46
+ "pet_id": order.get("petId"),
47
+ "quantity": order.get("quantity"),
48
+ },
49
+ },
50
+ })
@@ -0,0 +1,37 @@
1
+ import httpx
2
+ from typing import Dict, Any
3
+ from .types import Order, Pet
4
+
5
+ class PetStoreService:
6
+ async def create_pet(self, pet: Dict[str, Any]) -> Pet:
7
+ pet_data = {
8
+ "name": pet.get("name", ""),
9
+ "photoUrls": [pet.get("photo_url", "")],
10
+ "status": "available"
11
+ }
12
+
13
+ async with httpx.AsyncClient() as client:
14
+ response = await client.post(
15
+ 'https://petstore.swagger.io/v2/pet',
16
+ json=pet_data,
17
+ headers={'Content-Type': 'application/json'}
18
+ )
19
+ return response.json()
20
+
21
+ async def create_order(self, order: Dict[str, Any]) -> Order:
22
+ async with httpx.AsyncClient() as client:
23
+ order_data = {
24
+ "quantity": order.get("quantity", 1),
25
+ "petId": 1,
26
+ "shipDate": order.get("ship_date", "2025-08-22T22:07:04.730Z"),
27
+ "status": order.get("status", "placed"),
28
+ }
29
+
30
+ response = await client.post(
31
+ 'https://petstore.swagger.io/v2/store/order',
32
+ json=order_data,
33
+ headers={'Content-Type': 'application/json'}
34
+ )
35
+ return response.json()
36
+
37
+ pet_store_service = PetStoreService()
@@ -0,0 +1,19 @@
1
+ from pydantic import BaseModel
2
+ from enum import Enum
3
+
4
+ class OrderStatus(str, Enum):
5
+ PLACED = "placed"
6
+ APPROVED = "approved"
7
+ DELIVERED = "delivered"
8
+
9
+ class Pet(BaseModel):
10
+ id: int
11
+ name: str
12
+ photoUrl: str
13
+
14
+ class Order(BaseModel):
15
+ id: str
16
+ quantity: int
17
+ petId: int
18
+ shipDate: str
19
+ status: OrderStatus
@@ -0,0 +1,26 @@
1
+ [
2
+ {
3
+ "id": "step-configuration",
4
+ "title": "Step Configuration",
5
+ "description": "All steps should have a defined configuration, this is how you define the step's behavior and how it will be triggered.",
6
+ "lines": [
7
+ "3-10"
8
+ ]
9
+ },
10
+ {
11
+ "id": "cron-configuration",
12
+ "title": "Cron Configuration",
13
+ "description": "Cron steps require a specific configuration structure with the 'type' field set to 'cron' and a valid cron expression.",
14
+ "lines": [
15
+ "4-5"
16
+ ]
17
+ },
18
+ {
19
+ "id": "handler",
20
+ "title": "Cron Step Handler",
21
+ "description": "The Cron step handler only receives one argument.",
22
+ "lines": [
23
+ "12-39"
24
+ ]
25
+ }
26
+ ]
@@ -0,0 +1,39 @@
1
+ from datetime import datetime, timezone
2
+
3
+ config = {
4
+ "type": "cron",
5
+ "cron": "*/5 * * * *", # run every 5 minutes
6
+ "name": "PythonStateAuditJob",
7
+ "description": "Checks the state for orders that are not complete and have a ship date in the past",
8
+ "emits": ["python-notification"],
9
+ "flows": ["python-tutorial"],
10
+ }
11
+
12
+ async def handler(context):
13
+ state_value = await context.state.get_group("orders_python")
14
+
15
+ for item in state_value:
16
+ # check if current date is after item.ship_date
17
+ current_date = datetime.now(timezone.utc)
18
+ ship_date = datetime.fromisoformat(item.get("shipDate", "").replace('Z', '+00:00'))
19
+
20
+ if not item.get("complete", False) and current_date > ship_date:
21
+ context.logger.warn("Order is not complete and ship date is past", {
22
+ "order_id": item.get("id"),
23
+ "ship_date": item.get("shipDate"),
24
+ "complete": item.get("complete", False),
25
+ })
26
+
27
+ await context.emit({
28
+ "topic": "python-notification",
29
+ "data": {
30
+ "email": "test@test.com",
31
+ "template_id": "order-audit-warning",
32
+ "template_data": {
33
+ "order_id": item.get("id"),
34
+ "status": item.get("status"),
35
+ "ship_date": item.get("shipDate"),
36
+ "message": "Order is not complete and ship date is past",
37
+ },
38
+ },
39
+ })