motia 0.6.3-beta.130-601955 → 0.6.3-beta.130-426307

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 (141) hide show
  1. package/dist/cjs/cli.js +16 -40
  2. package/dist/cjs/create/index.d.ts +2 -2
  3. package/dist/cjs/create/index.js +3 -5
  4. package/dist/cjs/create/interactive.d.ts +2 -0
  5. package/dist/cjs/create/interactive.js +28 -19
  6. package/dist/cjs/create/pull-rules.d.ts +7 -0
  7. package/dist/cjs/create/pull-rules.js +28 -0
  8. package/dist/cjs/create/setup-template.js +5 -3
  9. package/dist/cjs/create/templates/index.js +1 -1
  10. package/dist/cjs/create/templates/index.ts +1 -1
  11. package/dist/cjs/cursor-rules/dot-files/.claude/CLAUDE.md +467 -0
  12. package/dist/cjs/cursor-rules/dot-files/.claude/README.md +97 -0
  13. package/dist/cjs/cursor-rules/dot-files/.claude/agents/code-reviewer.md +153 -0
  14. package/dist/cjs/cursor-rules/dot-files/.claude/agents/debugger.md +259 -0
  15. package/dist/cjs/cursor-rules/dot-files/.claude/agents/test-runner.md +268 -0
  16. package/dist/cjs/cursor-rules/dot-files/.claude/commands/add-authentication.md +491 -0
  17. package/dist/cjs/cursor-rules/dot-files/.claude/commands/ai-ml-patterns.md +748 -0
  18. package/dist/cjs/cursor-rules/dot-files/.claude/commands/authentication.md +515 -0
  19. package/dist/cjs/cursor-rules/dot-files/.claude/commands/backend-types.md +719 -0
  20. package/dist/cjs/cursor-rules/dot-files/.claude/commands/build-api.md +407 -0
  21. package/dist/cjs/cursor-rules/dot-files/.claude/commands/claude-workflows.md +1032 -0
  22. package/dist/cjs/cursor-rules/dot-files/.claude/commands/complete-backend.md +345 -0
  23. package/dist/cjs/cursor-rules/dot-files/.claude/commands/create-api.md +96 -0
  24. package/dist/cjs/cursor-rules/dot-files/.claude/commands/data-processing.md +977 -0
  25. package/dist/cjs/cursor-rules/dot-files/.claude/commands/integrate-ai.md +852 -0
  26. package/dist/cjs/cursor-rules/dot-files/.claude/commands/javascript-patterns.md +678 -0
  27. package/dist/cjs/cursor-rules/dot-files/.claude/commands/multi-language-workflow.md +756 -0
  28. package/dist/cjs/cursor-rules/dot-files/.claude/commands/multi-language.md +141 -0
  29. package/dist/cjs/cursor-rules/dot-files/.claude/commands/process-background-jobs.md +587 -0
  30. package/dist/cjs/cursor-rules/dot-files/.claude/commands/process-events.md +89 -0
  31. package/dist/cjs/cursor-rules/dot-files/.claude/hooks/pre-commit.sh +84 -0
  32. package/dist/cjs/cursor-rules/dot-files/.claude/settings.json +37 -0
  33. package/dist/cjs/cursor-rules/dot-files/.cursor/rules/ai-agent-patterns.mdc +725 -0
  34. package/dist/cjs/cursor-rules/dot-files/.cursor/rules/api-design-patterns.mdc +740 -0
  35. package/dist/cjs/cursor-rules/dot-files/.cursor/rules/api-steps.mdc +230 -0
  36. package/dist/cjs/cursor-rules/dot-files/.cursor/rules/architecture.mdc +189 -0
  37. package/dist/cjs/cursor-rules/dot-files/.cursor/rules/authentication-patterns.mdc +620 -0
  38. package/dist/cjs/cursor-rules/dot-files/.cursor/rules/background-job-patterns.mdc +628 -0
  39. package/dist/cjs/cursor-rules/dot-files/.cursor/rules/complete-application-patterns.mdc +433 -0
  40. package/dist/cjs/cursor-rules/dot-files/.cursor/rules/complete-backend-generator.mdc +415 -0
  41. package/dist/cjs/cursor-rules/dot-files/.cursor/rules/cron-steps.mdc +257 -0
  42. package/dist/cjs/cursor-rules/dot-files/.cursor/rules/event-steps.mdc +504 -0
  43. package/dist/cjs/cursor-rules/dot-files/.cursor/rules/instructions.mdc +15 -0
  44. package/dist/cjs/cursor-rules/dot-files/.cursor/rules/multi-language-workflows.mdc +1059 -0
  45. package/dist/cjs/cursor-rules/dot-files/.cursor/rules/noop-steps.mdc +57 -0
  46. package/dist/cjs/cursor-rules/dot-files/.cursor/rules/production-deployment.mdc +668 -0
  47. package/dist/cjs/cursor-rules/dot-files/.cursor/rules/realtime-streaming.mdc +656 -0
  48. package/dist/cjs/cursor-rules/dot-files/.cursor/rules/state-management.mdc +371 -0
  49. package/dist/cjs/cursor-rules/dot-files/.cursor/rules/steps.mdc +373 -0
  50. package/dist/cjs/cursor-rules/dot-files/.cursor/rules/testing.mdc +329 -0
  51. package/dist/cjs/cursor-rules/dot-files/.cursor/rules/typescript.mdc +409 -0
  52. package/dist/cjs/cursor-rules/dot-files/.cursor/rules/ui-steps.mdc +90 -0
  53. package/dist/cjs/cursor-rules/dot-files/.cursor/rules/workflow-patterns.mdc +938 -0
  54. package/dist/cjs/cursor-rules/dot-files/AGENTS.md +397 -0
  55. package/dist/cjs/cursor-rules/dot-files/README.md +58 -0
  56. package/dist/esm/cli.js +16 -40
  57. package/dist/esm/create/index.d.ts +2 -2
  58. package/dist/esm/create/index.js +3 -5
  59. package/dist/esm/create/interactive.d.ts +2 -0
  60. package/dist/esm/create/interactive.js +28 -19
  61. package/dist/esm/create/pull-rules.d.ts +7 -0
  62. package/dist/esm/create/pull-rules.js +21 -0
  63. package/dist/esm/create/setup-template.js +5 -3
  64. package/dist/esm/create/templates/index.js +1 -1
  65. package/dist/esm/create/templates/index.ts +1 -1
  66. package/dist/esm/cursor-rules/dot-files/.claude/CLAUDE.md +467 -0
  67. package/dist/esm/cursor-rules/dot-files/.claude/README.md +97 -0
  68. package/dist/esm/cursor-rules/dot-files/.claude/agents/code-reviewer.md +153 -0
  69. package/dist/esm/cursor-rules/dot-files/.claude/agents/debugger.md +259 -0
  70. package/dist/esm/cursor-rules/dot-files/.claude/agents/test-runner.md +268 -0
  71. package/dist/esm/cursor-rules/dot-files/.claude/commands/add-authentication.md +491 -0
  72. package/dist/esm/cursor-rules/dot-files/.claude/commands/ai-ml-patterns.md +748 -0
  73. package/dist/esm/cursor-rules/dot-files/.claude/commands/authentication.md +515 -0
  74. package/dist/esm/cursor-rules/dot-files/.claude/commands/backend-types.md +719 -0
  75. package/dist/esm/cursor-rules/dot-files/.claude/commands/build-api.md +407 -0
  76. package/dist/esm/cursor-rules/dot-files/.claude/commands/claude-workflows.md +1032 -0
  77. package/dist/esm/cursor-rules/dot-files/.claude/commands/complete-backend.md +345 -0
  78. package/dist/esm/cursor-rules/dot-files/.claude/commands/create-api.md +96 -0
  79. package/dist/esm/cursor-rules/dot-files/.claude/commands/data-processing.md +977 -0
  80. package/dist/esm/cursor-rules/dot-files/.claude/commands/integrate-ai.md +852 -0
  81. package/dist/esm/cursor-rules/dot-files/.claude/commands/javascript-patterns.md +678 -0
  82. package/dist/esm/cursor-rules/dot-files/.claude/commands/multi-language-workflow.md +756 -0
  83. package/dist/esm/cursor-rules/dot-files/.claude/commands/multi-language.md +141 -0
  84. package/dist/esm/cursor-rules/dot-files/.claude/commands/process-background-jobs.md +587 -0
  85. package/dist/esm/cursor-rules/dot-files/.claude/commands/process-events.md +89 -0
  86. package/dist/esm/cursor-rules/dot-files/.claude/hooks/pre-commit.sh +84 -0
  87. package/dist/esm/cursor-rules/dot-files/.claude/settings.json +37 -0
  88. package/dist/esm/cursor-rules/dot-files/.cursor/rules/ai-agent-patterns.mdc +725 -0
  89. package/dist/esm/cursor-rules/dot-files/.cursor/rules/api-design-patterns.mdc +740 -0
  90. package/dist/esm/cursor-rules/dot-files/.cursor/rules/api-steps.mdc +230 -0
  91. package/dist/esm/cursor-rules/dot-files/.cursor/rules/architecture.mdc +189 -0
  92. package/dist/esm/cursor-rules/dot-files/.cursor/rules/authentication-patterns.mdc +620 -0
  93. package/dist/esm/cursor-rules/dot-files/.cursor/rules/background-job-patterns.mdc +628 -0
  94. package/dist/esm/cursor-rules/dot-files/.cursor/rules/complete-application-patterns.mdc +433 -0
  95. package/dist/esm/cursor-rules/dot-files/.cursor/rules/complete-backend-generator.mdc +415 -0
  96. package/dist/esm/cursor-rules/dot-files/.cursor/rules/cron-steps.mdc +257 -0
  97. package/dist/esm/cursor-rules/dot-files/.cursor/rules/event-steps.mdc +504 -0
  98. package/dist/esm/cursor-rules/dot-files/.cursor/rules/instructions.mdc +15 -0
  99. package/dist/esm/cursor-rules/dot-files/.cursor/rules/multi-language-workflows.mdc +1059 -0
  100. package/dist/esm/cursor-rules/dot-files/.cursor/rules/noop-steps.mdc +57 -0
  101. package/dist/esm/cursor-rules/dot-files/.cursor/rules/production-deployment.mdc +668 -0
  102. package/dist/esm/cursor-rules/dot-files/.cursor/rules/realtime-streaming.mdc +656 -0
  103. package/dist/esm/cursor-rules/dot-files/.cursor/rules/state-management.mdc +371 -0
  104. package/dist/esm/cursor-rules/dot-files/.cursor/rules/steps.mdc +373 -0
  105. package/dist/esm/cursor-rules/dot-files/.cursor/rules/testing.mdc +329 -0
  106. package/dist/esm/cursor-rules/dot-files/.cursor/rules/typescript.mdc +409 -0
  107. package/dist/esm/cursor-rules/dot-files/.cursor/rules/ui-steps.mdc +90 -0
  108. package/dist/esm/cursor-rules/dot-files/.cursor/rules/workflow-patterns.mdc +938 -0
  109. package/dist/esm/cursor-rules/dot-files/AGENTS.md +397 -0
  110. package/dist/esm/cursor-rules/dot-files/README.md +58 -0
  111. package/dist/types/create/index.d.ts +2 -2
  112. package/dist/types/create/interactive.d.ts +2 -0
  113. package/dist/types/create/pull-rules.d.ts +7 -0
  114. package/package.json +4 -4
  115. package/dist/cjs/cursor-rules/index.d.ts +0 -8
  116. package/dist/cjs/cursor-rules/index.js +0 -269
  117. package/dist/esm/cursor-rules/index.d.ts +0 -8
  118. package/dist/esm/cursor-rules/index.js +0 -263
  119. package/dist/types/cursor-rules/index.d.ts +0 -8
  120. /package/dist/cjs/create/templates/{typescript → nodejs}/motia-workbench.json +0 -0
  121. /package/dist/cjs/create/templates/{typescript → nodejs}/services/pet-store.ts.txt +0 -0
  122. /package/dist/cjs/create/templates/{typescript → nodejs}/services/types.ts.txt +0 -0
  123. /package/dist/cjs/create/templates/{typescript → nodejs}/steps/api.step.ts-features.json.txt +0 -0
  124. /package/dist/cjs/create/templates/{typescript → nodejs}/steps/api.step.ts.txt +0 -0
  125. /package/dist/cjs/create/templates/{typescript → nodejs}/steps/notification.step.ts.txt +0 -0
  126. /package/dist/cjs/create/templates/{typescript → nodejs}/steps/process-food-order.step.ts-features.json.txt +0 -0
  127. /package/dist/cjs/create/templates/{typescript → nodejs}/steps/process-food-order.step.ts.txt +0 -0
  128. /package/dist/cjs/create/templates/{typescript → nodejs}/steps/state-audit-cron.step.ts-features.json.txt +0 -0
  129. /package/dist/cjs/create/templates/{typescript → nodejs}/steps/state-audit-cron.step.ts.txt +0 -0
  130. /package/dist/cjs/create/templates/{typescript → nodejs}/tutorial.tsx.txt +0 -0
  131. /package/dist/esm/create/templates/{typescript → nodejs}/motia-workbench.json +0 -0
  132. /package/dist/esm/create/templates/{typescript → nodejs}/services/pet-store.ts.txt +0 -0
  133. /package/dist/esm/create/templates/{typescript → nodejs}/services/types.ts.txt +0 -0
  134. /package/dist/esm/create/templates/{typescript → nodejs}/steps/api.step.ts-features.json.txt +0 -0
  135. /package/dist/esm/create/templates/{typescript → nodejs}/steps/api.step.ts.txt +0 -0
  136. /package/dist/esm/create/templates/{typescript → nodejs}/steps/notification.step.ts.txt +0 -0
  137. /package/dist/esm/create/templates/{typescript → nodejs}/steps/process-food-order.step.ts-features.json.txt +0 -0
  138. /package/dist/esm/create/templates/{typescript → nodejs}/steps/process-food-order.step.ts.txt +0 -0
  139. /package/dist/esm/create/templates/{typescript → nodejs}/steps/state-audit-cron.step.ts-features.json.txt +0 -0
  140. /package/dist/esm/create/templates/{typescript → nodejs}/steps/state-audit-cron.step.ts.txt +0 -0
  141. /package/dist/esm/create/templates/{typescript → nodejs}/tutorial.tsx.txt +0 -0
@@ -0,0 +1,628 @@
1
+ ---
2
+ description: Background job patterns for handling asynchronous tasks, scheduled jobs, and long-running processes
3
+ globs:
4
+ alwaysApply: false
5
+ ---
6
+ # Background Job Patterns
7
+
8
+ Implement robust background processing systems for asynchronous tasks, scheduled jobs, and long-running processes.
9
+
10
+ ## Event-Driven Job Processing
11
+
12
+ ### Job Queue System
13
+
14
+ ```typescript
15
+ // steps/jobs/job-processor.step.ts
16
+ import { EventConfig, Handlers } from 'motia'
17
+ import { z } from 'zod'
18
+
19
+ export const config: EventConfig = {
20
+ type: 'event',
21
+ name: 'JobProcessor',
22
+ description: 'Process background jobs from the job queue',
23
+ subscribes: ['job.enqueued', 'job.retry'],
24
+ emits: ['job.started', 'job.completed', 'job.failed', 'job.retry.scheduled'],
25
+ input: z.object({
26
+ jobId: z.string(),
27
+ type: z.string(),
28
+ payload: z.record(z.any()),
29
+ priority: z.enum(['low', 'medium', 'high']).default('medium'),
30
+ attempts: z.number().default(0),
31
+ maxAttempts: z.number().default(3),
32
+ delayUntil: z.string().optional(),
33
+ createdBy: z.string().optional()
34
+ }),
35
+ flows: ['job-processing']
36
+ }
37
+
38
+ export const handler: Handlers['JobProcessor'] = async (input, { emit, logger, state }) => {
39
+ const { jobId, type, payload, attempts, maxAttempts, createdBy } = input
40
+
41
+ try {
42
+ // Update job status to processing
43
+ await state.set('jobs', jobId, {
44
+ ...input,
45
+ status: 'processing',
46
+ startedAt: new Date().toISOString(),
47
+ attempts: attempts + 1
48
+ })
49
+
50
+ await emit({
51
+ topic: 'job.started',
52
+ data: { jobId, type, attempt: attempts + 1 }
53
+ })
54
+
55
+ // Process job based on type
56
+ const result = await processJob(type, payload, { state, logger })
57
+
58
+ // Mark job as completed
59
+ await state.set('jobs', jobId, {
60
+ ...input,
61
+ status: 'completed',
62
+ result,
63
+ completedAt: new Date().toISOString(),
64
+ attempts: attempts + 1
65
+ })
66
+
67
+ await emit({
68
+ topic: 'job.completed',
69
+ data: { jobId, type, result, duration: calculateDuration(input) }
70
+ })
71
+
72
+ logger.info('Job completed successfully', { jobId, type, attempts: attempts + 1 })
73
+
74
+ } catch (error) {
75
+ const currentAttempts = attempts + 1
76
+ logger.error('Job processing failed', {
77
+ error: error.message,
78
+ jobId,
79
+ type,
80
+ attempts: currentAttempts
81
+ })
82
+
83
+ // Check if we should retry
84
+ if (currentAttempts < maxAttempts && isRetryableError(error)) {
85
+ const delay = calculateRetryDelay(currentAttempts)
86
+ const retryAt = new Date(Date.now() + delay).toISOString()
87
+
88
+ await state.set('jobs', jobId, {
89
+ ...input,
90
+ status: 'failed',
91
+ error: error.message,
92
+ attempts: currentAttempts,
93
+ retryAt,
94
+ lastFailedAt: new Date().toISOString()
95
+ })
96
+
97
+ await emit({
98
+ topic: 'job.retry.scheduled',
99
+ data: {
100
+ jobId,
101
+ type,
102
+ attempt: currentAttempts,
103
+ retryAt,
104
+ delay
105
+ }
106
+ })
107
+ } else {
108
+ // Max attempts reached or non-retryable error
109
+ await state.set('jobs', jobId, {
110
+ ...input,
111
+ status: 'failed',
112
+ error: error.message,
113
+ attempts: currentAttempts,
114
+ failedAt: new Date().toISOString()
115
+ })
116
+
117
+ await emit({
118
+ topic: 'job.failed',
119
+ data: {
120
+ jobId,
121
+ type,
122
+ error: error.message,
123
+ finalAttempt: currentAttempts,
124
+ createdBy
125
+ }
126
+ })
127
+ }
128
+ }
129
+ }
130
+
131
+ async function processJob(type: string, payload: any, context: any) {
132
+ switch (type) {
133
+ case 'email':
134
+ return await processEmailJob(payload, context)
135
+ case 'image_processing':
136
+ return await processImageJob(payload, context)
137
+ case 'data_export':
138
+ return await processDataExportJob(payload, context)
139
+ case 'report_generation':
140
+ return await processReportJob(payload, context)
141
+ default:
142
+ throw new Error(`Unknown job type: ${type}`)
143
+ }
144
+ }
145
+
146
+ function isRetryableError(error: any): boolean {
147
+ // Define which errors should be retried
148
+ const retryableErrors = [
149
+ 'NETWORK_ERROR',
150
+ 'TIMEOUT',
151
+ 'RATE_LIMIT',
152
+ 'TEMPORARY_FAILURE'
153
+ ]
154
+
155
+ return retryableErrors.some(type =>
156
+ error.message.includes(type) || error.code === type
157
+ )
158
+ }
159
+
160
+ function calculateRetryDelay(attempt: number): number {
161
+ // Exponential backoff with jitter
162
+ const baseDelay = 1000 // 1 second
163
+ const maxDelay = 300000 // 5 minutes
164
+ const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay)
165
+
166
+ // Add jitter (±25%)
167
+ const jitter = delay * 0.25 * (Math.random() - 0.5)
168
+ return Math.round(delay + jitter)
169
+ }
170
+ ```
171
+
172
+ ### Job Scheduler
173
+
174
+ ```typescript
175
+ // steps/jobs/job-scheduler.step.ts
176
+ import { CronConfig, Handlers } from 'motia'
177
+
178
+ export const config: CronConfig = {
179
+ type: 'cron',
180
+ name: 'JobScheduler',
181
+ description: 'Process scheduled and delayed jobs',
182
+ cron: '*/10 * * * * *', // Every 10 seconds
183
+ emits: ['job.enqueued'],
184
+ flows: ['job-scheduling']
185
+ }
186
+
187
+ export const handler: Handlers['JobScheduler'] = async ({ emit, logger, state }) => {
188
+ try {
189
+ const now = new Date().toISOString()
190
+
191
+ // Get jobs that are ready to be processed
192
+ const readyJobs = await getJobsReadyForProcessing(now, state)
193
+
194
+ for (const job of readyJobs) {
195
+ // Emit job for processing
196
+ await emit({
197
+ topic: 'job.enqueued',
198
+ data: job
199
+ })
200
+
201
+ // Update job status
202
+ await state.set('jobs', job.jobId, {
203
+ ...job,
204
+ status: 'queued',
205
+ queuedAt: now
206
+ })
207
+ }
208
+
209
+ if (readyJobs.length > 0) {
210
+ logger.info('Jobs scheduled for processing', { count: readyJobs.length })
211
+ }
212
+
213
+ // Clean up old completed jobs
214
+ await cleanupOldJobs(state, logger)
215
+
216
+ } catch (error) {
217
+ logger.error('Job scheduler failed', { error: error.message })
218
+ }
219
+ }
220
+
221
+ async function getJobsReadyForProcessing(now: string, state: any) {
222
+ // Get all jobs that are scheduled or need retry
223
+ const allJobs = await state.getGroup('jobs') || {}
224
+
225
+ return Object.values(allJobs).filter((job: any) => {
226
+ return (job.status === 'scheduled' && job.delayUntil && job.delayUntil <= now) ||
227
+ (job.status === 'failed' && job.retryAt && job.retryAt <= now)
228
+ })
229
+ }
230
+
231
+ async function cleanupOldJobs(state: any, logger: any) {
232
+ const cutoffDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString()
233
+ const allJobs = await state.getGroup('jobs') || {}
234
+
235
+ let cleanedCount = 0
236
+ for (const [jobId, job] of Object.entries(allJobs)) {
237
+ if ((job as any).status === 'completed' && (job as any).completedAt < cutoffDate) {
238
+ await state.delete('jobs', jobId)
239
+ cleanedCount++
240
+ }
241
+ }
242
+
243
+ if (cleanedCount > 0) {
244
+ logger.info('Cleaned up old completed jobs', { count: cleanedCount })
245
+ }
246
+ }
247
+ ```
248
+
249
+ ## Specific Job Types
250
+
251
+ ### Email Processing Job
252
+
253
+ ```typescript
254
+ // steps/jobs/email-processor.step.ts
255
+ import { EventConfig, Handlers } from 'motia'
256
+ import { z } from 'zod'
257
+
258
+ export const config: EventConfig = {
259
+ type: 'event',
260
+ name: 'EmailProcessor',
261
+ description: 'Process email sending jobs',
262
+ subscribes: ['email.send.requested'],
263
+ emits: ['job.enqueued'],
264
+ input: z.object({
265
+ to: z.union([z.string().email(), z.array(z.string().email())]),
266
+ subject: z.string(),
267
+ templateId: z.string().optional(),
268
+ htmlContent: z.string().optional(),
269
+ textContent: z.string().optional(),
270
+ variables: z.record(z.any()).optional(),
271
+ attachments: z.array(z.object({
272
+ filename: z.string(),
273
+ content: z.string(), // base64 encoded
274
+ contentType: z.string()
275
+ })).optional(),
276
+ priority: z.enum(['low', 'medium', 'high']).default('medium'),
277
+ sendAt: z.string().optional(), // ISO timestamp for scheduled sending
278
+ userId: z.string().optional()
279
+ }),
280
+ flows: ['email-processing']
281
+ }
282
+
283
+ export const handler: Handlers['EmailProcessor'] = async (input, { emit, logger }) => {
284
+ try {
285
+ // Create email job
286
+ const jobId = crypto.randomUUID()
287
+ const emailJob = {
288
+ jobId,
289
+ type: 'email',
290
+ payload: input,
291
+ priority: input.priority,
292
+ maxAttempts: 3,
293
+ delayUntil: input.sendAt || new Date().toISOString(),
294
+ createdBy: input.userId
295
+ }
296
+
297
+ // Enqueue the job
298
+ await emit({
299
+ topic: 'job.enqueued',
300
+ data: emailJob
301
+ })
302
+
303
+ logger.info('Email job created', {
304
+ jobId,
305
+ recipients: Array.isArray(input.to) ? input.to.length : 1,
306
+ scheduled: !!input.sendAt
307
+ })
308
+
309
+ } catch (error) {
310
+ logger.error('Failed to create email job', { error: error.message })
311
+ }
312
+ }
313
+
314
+ async function processEmailJob(payload: any, context: any) {
315
+ const { to, subject, templateId, htmlContent, textContent, variables, attachments } = payload
316
+
317
+ try {
318
+ let finalHtmlContent = htmlContent
319
+ let finalTextContent = textContent
320
+
321
+ // Process template if provided
322
+ if (templateId) {
323
+ const template = await context.state.get('email-templates', templateId)
324
+ if (template) {
325
+ finalHtmlContent = processTemplate(template.html, variables || {})
326
+ finalTextContent = processTemplate(template.text, variables || {})
327
+ }
328
+ }
329
+
330
+ // Send email(s)
331
+ const recipients = Array.isArray(to) ? to : [to]
332
+ const results = []
333
+
334
+ for (const recipient of recipients) {
335
+ const result = await sendEmail({
336
+ to: recipient,
337
+ subject,
338
+ html: finalHtmlContent,
339
+ text: finalTextContent,
340
+ attachments
341
+ })
342
+ results.push({ recipient, messageId: result.messageId })
343
+ }
344
+
345
+ return {
346
+ sent: results.length,
347
+ results,
348
+ timestamp: new Date().toISOString()
349
+ }
350
+ } catch (error) {
351
+ throw new Error(`Email sending failed: ${error.message}`)
352
+ }
353
+ }
354
+
355
+ function processTemplate(template: string, variables: Record<string, any>): string {
356
+ // Simple template variable substitution
357
+ return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
358
+ return variables[key] || match
359
+ })
360
+ }
361
+
362
+ async function sendEmail(emailData: any) {
363
+ // Email service integration (SendGrid, SES, etc.)
364
+ return { messageId: crypto.randomUUID() }
365
+ }
366
+ ```
367
+
368
+ ### Data Processing Job
369
+
370
+ ```typescript
371
+ // steps/jobs/data-processor.step.ts
372
+ import { EventConfig, Handlers } from 'motia'
373
+ import { z } from 'zod'
374
+
375
+ export const config: EventConfig = {
376
+ type: 'event',
377
+ name: 'DataProcessor',
378
+ description: 'Process large datasets in the background',
379
+ subscribes: ['data.processing.requested'],
380
+ emits: ['job.enqueued'],
381
+ input: z.object({
382
+ dataSource: z.string(), // file path, database query, etc.
383
+ processingType: z.enum(['transform', 'analyze', 'export', 'import']),
384
+ batchSize: z.number().default(1000),
385
+ outputDestination: z.string(),
386
+ filters: z.record(z.any()).optional(),
387
+ transformations: z.array(z.record(z.any())).optional(),
388
+ userId: z.string(),
389
+ priority: z.enum(['low', 'medium', 'high']).default('medium')
390
+ }),
391
+ flows: ['data-processing']
392
+ }
393
+
394
+ export const handler: Handlers['DataProcessor'] = async (input, { emit, logger }) => {
395
+ try {
396
+ const jobId = crypto.randomUUID()
397
+
398
+ // Create data processing job
399
+ const dataJob = {
400
+ jobId,
401
+ type: 'data_processing',
402
+ payload: input,
403
+ priority: input.priority,
404
+ maxAttempts: 2, // Data jobs typically shouldn't be retried many times
405
+ createdBy: input.userId
406
+ }
407
+
408
+ await emit({
409
+ topic: 'job.enqueued',
410
+ data: dataJob
411
+ })
412
+
413
+ logger.info('Data processing job created', {
414
+ jobId,
415
+ type: input.processingType,
416
+ source: input.dataSource
417
+ })
418
+
419
+ } catch (error) {
420
+ logger.error('Failed to create data processing job', { error: error.message })
421
+ }
422
+ }
423
+
424
+ async function processDataProcessingJob(payload: any, context: any) {
425
+ const { dataSource, processingType, batchSize, outputDestination, filters, transformations } = payload
426
+
427
+ try {
428
+ // Initialize processing
429
+ const processingStats = {
430
+ startTime: new Date().toISOString(),
431
+ totalRecords: 0,
432
+ processedRecords: 0,
433
+ errorRecords: 0,
434
+ batches: 0
435
+ }
436
+
437
+ // Get data source
438
+ const dataIterator = await createDataIterator(dataSource, batchSize, filters)
439
+
440
+ // Process data in batches
441
+ for await (const batch of dataIterator) {
442
+ processingStats.batches++
443
+
444
+ try {
445
+ const processedBatch = await processBatch(batch, processingType, transformations)
446
+ await writeOutput(processedBatch, outputDestination)
447
+
448
+ processingStats.processedRecords += batch.length
449
+ } catch (error) {
450
+ context.logger.error('Batch processing failed', {
451
+ batch: processingStats.batches,
452
+ error: error.message
453
+ })
454
+ processingStats.errorRecords += batch.length
455
+ }
456
+
457
+ processingStats.totalRecords += batch.length
458
+
459
+ // Update progress periodically
460
+ if (processingStats.batches % 10 === 0) {
461
+ await updateProcessingProgress(processingStats, context.state)
462
+ }
463
+ }
464
+
465
+ processingStats.endTime = new Date().toISOString()
466
+ processingStats.duration = new Date(processingStats.endTime).getTime() -
467
+ new Date(processingStats.startTime).getTime()
468
+
469
+ return processingStats
470
+ } catch (error) {
471
+ throw new Error(`Data processing failed: ${error.message}`)
472
+ }
473
+ }
474
+
475
+ async function* createDataIterator(source: string, batchSize: number, filters: any) {
476
+ // Implementation would depend on data source type
477
+ // This is a simplified example
478
+ const totalRecords = 10000 // Would be determined from actual source
479
+
480
+ for (let offset = 0; offset < totalRecords; offset += batchSize) {
481
+ const batch = await fetchDataBatch(source, offset, batchSize, filters)
482
+ if (batch.length === 0) break
483
+ yield batch
484
+ }
485
+ }
486
+
487
+ async function processBatch(batch: any[], type: string, transformations: any[]) {
488
+ switch (type) {
489
+ case 'transform':
490
+ return applyTransformations(batch, transformations || [])
491
+ case 'analyze':
492
+ return analyzeBatch(batch)
493
+ default:
494
+ return batch
495
+ }
496
+ }
497
+ ```
498
+
499
+ ## Long-Running Process Management
500
+
501
+ ### Process Monitor
502
+
503
+ ```typescript
504
+ // steps/jobs/process-monitor.step.ts
505
+ import { CronConfig, Handlers } from 'motia'
506
+
507
+ export const config: CronConfig = {
508
+ type: 'cron',
509
+ name: 'ProcessMonitor',
510
+ description: 'Monitor long-running processes and handle timeouts',
511
+ cron: '*/30 * * * * *', // Every 30 seconds
512
+ emits: ['process.timeout', 'process.status.updated'],
513
+ flows: ['process-monitoring']
514
+ }
515
+
516
+ export const handler: Handlers['ProcessMonitor'] = async ({ emit, logger, state }) => {
517
+ try {
518
+ const now = Date.now()
519
+ const timeoutThreshold = 30 * 60 * 1000 // 30 minutes
520
+
521
+ // Get all running processes
522
+ const runningJobs = await getRunningJobs(state)
523
+
524
+ for (const job of runningJobs) {
525
+ const startTime = new Date(job.startedAt).getTime()
526
+ const runningTime = now - startTime
527
+
528
+ // Check for timeout
529
+ if (runningTime > timeoutThreshold) {
530
+ await handleJobTimeout(job, runningTime, { emit, state, logger })
531
+ }
532
+
533
+ // Update process status
534
+ await updateJobStatus(job, runningTime, { emit, state })
535
+ }
536
+
537
+ // Generate process health report
538
+ await generateProcessHealthReport(runningJobs, { emit, logger })
539
+
540
+ } catch (error) {
541
+ logger.error('Process monitoring failed', { error: error.message })
542
+ }
543
+ }
544
+
545
+ async function getRunningJobs(state: any) {
546
+ const allJobs = await state.getGroup('jobs') || {}
547
+ return Object.values(allJobs).filter((job: any) =>
548
+ job.status === 'processing' && job.startedAt
549
+ )
550
+ }
551
+
552
+ async function handleJobTimeout(job: any, runningTime: number, context: any) {
553
+ const { emit, state, logger } = context
554
+
555
+ // Mark job as timed out
556
+ await state.set('jobs', job.jobId, {
557
+ ...job,
558
+ status: 'timeout',
559
+ timedOutAt: new Date().toISOString(),
560
+ runningTime
561
+ })
562
+
563
+ await emit({
564
+ topic: 'process.timeout',
565
+ data: {
566
+ jobId: job.jobId,
567
+ type: job.type,
568
+ runningTime,
569
+ startedAt: job.startedAt
570
+ }
571
+ })
572
+
573
+ logger.warn('Job timed out', {
574
+ jobId: job.jobId,
575
+ type: job.type,
576
+ runningTime
577
+ })
578
+ }
579
+
580
+ async function generateProcessHealthReport(runningJobs: any[], context: any) {
581
+ const report = {
582
+ totalRunning: runningJobs.length,
583
+ byType: {},
584
+ averageRuntime: 0,
585
+ longestRunning: null,
586
+ timestamp: new Date().toISOString()
587
+ }
588
+
589
+ const now = Date.now()
590
+ let totalRuntime = 0
591
+
592
+ for (const job of runningJobs) {
593
+ const runtime = now - new Date(job.startedAt).getTime()
594
+ totalRuntime += runtime
595
+
596
+ // Group by type
597
+ if (!report.byType[job.type]) {
598
+ report.byType[job.type] = { count: 0, totalRuntime: 0 }
599
+ }
600
+ report.byType[job.type].count++
601
+ report.byType[job.type].totalRuntime += runtime
602
+
603
+ // Track longest running
604
+ if (!report.longestRunning || runtime > (now - new Date(report.longestRunning.startedAt).getTime())) {
605
+ report.longestRunning = { ...job, runtime }
606
+ }
607
+ }
608
+
609
+ if (runningJobs.length > 0) {
610
+ report.averageRuntime = totalRuntime / runningJobs.length
611
+ }
612
+
613
+ await context.emit({
614
+ topic: 'process.status.updated',
615
+ data: report
616
+ })
617
+ }
618
+ ```
619
+
620
+ This background job system provides:
621
+ - Event-driven job processing with retry logic
622
+ - Job scheduling and delayed execution
623
+ - Specialized processors for different job types
624
+ - Progress tracking and monitoring
625
+ - Timeout handling and process health monitoring
626
+ - Batch processing capabilities
627
+ - Error handling and failure recovery
628
+ - Job cleanup and maintenance