@elevasis/sdk 0.4.6 → 0.4.8

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 (41) hide show
  1. package/dist/cli.cjs +815 -452
  2. package/dist/index.d.ts +79 -14
  3. package/dist/index.js +17 -12
  4. package/dist/templates.js +747 -0
  5. package/dist/types/templates.d.ts +1 -0
  6. package/dist/types/worker/index.d.ts +6 -0
  7. package/dist/types/worker/platform.d.ts +32 -0
  8. package/dist/worker/index.js +4728 -11
  9. package/package.json +16 -10
  10. package/reference/_index.md +48 -6
  11. package/reference/_navigation.md +104 -0
  12. package/reference/cli/index.mdx +2 -1
  13. package/reference/concepts/index.mdx +203 -0
  14. package/reference/deployment/api.mdx +1 -0
  15. package/reference/deployment/index.mdx +1 -0
  16. package/reference/developer/interaction-guidance.mdx +213 -0
  17. package/reference/framework/agent.mdx +175 -0
  18. package/reference/{documentation/index.mdx → framework/documentation.mdx} +1 -0
  19. package/reference/framework/index.mdx +95 -0
  20. package/reference/framework/memory.mdx +337 -0
  21. package/reference/framework/project-structure.mdx +294 -0
  22. package/reference/getting-started/index.mdx +39 -15
  23. package/reference/index.mdx +10 -2
  24. package/reference/platform-tools/examples.mdx +1 -0
  25. package/reference/platform-tools/index.mdx +43 -2
  26. package/reference/resources/index.mdx +1 -0
  27. package/reference/resources/patterns.mdx +2 -1
  28. package/reference/resources/types.mdx +1 -0
  29. package/reference/roadmap/index.mdx +1 -0
  30. package/reference/runtime/index.mdx +1 -0
  31. package/reference/runtime/limits.mdx +1 -0
  32. package/reference/security/credentials.mdx +141 -0
  33. package/reference/templates/data-enrichment.mdx +162 -0
  34. package/reference/templates/email-sender.mdx +135 -0
  35. package/reference/templates/lead-scorer.mdx +175 -0
  36. package/reference/templates/pdf-generator.mdx +151 -0
  37. package/reference/templates/recurring-job.mdx +189 -0
  38. package/reference/templates/text-classifier.mdx +147 -0
  39. package/reference/templates/web-scraper.mdx +135 -0
  40. package/reference/troubleshooting/common-errors.mdx +210 -0
  41. package/reference/getting-started/project-structure.mdx +0 -148
@@ -0,0 +1,135 @@
1
+ ---
2
+ title: "Template: Email Sender"
3
+ description: "Transactional email via Resend with template support -- send styled emails to one or multiple recipients"
4
+ loadWhen: "Applying the email-sender workflow template"
5
+ ---
6
+
7
+ **Category:** Communication
8
+
9
+ **Platform Tools:** `resend` (send email)
10
+
11
+ **Credentials Required:**
12
+
13
+ - `my-resend` -- Resend API key (bearer type)
14
+
15
+ ---
16
+
17
+ ## What This Workflow Does
18
+
19
+ Sends a transactional email to one or more recipients using the Resend email service. Supports plain text and HTML content, reply-to addresses, and CC/BCC. Suitable for welcome emails, notification emails, confirmation emails, and any triggered transactional communication.
20
+
21
+ ---
22
+
23
+ ## Input Schema
24
+
25
+ ```typescript
26
+ z.object({
27
+ to: z.union([z.string(), z.array(z.string())]), // Recipient(s)
28
+ subject: z.string(), // Email subject
29
+ html: z.string().optional(), // HTML body
30
+ text: z.string().optional(), // Plain text body (fallback)
31
+ from: z.string().optional(), // Sender (defaults to workspace default)
32
+ replyTo: z.string().optional(), // Reply-to address
33
+ })
34
+ ```
35
+
36
+ ## Output Schema
37
+
38
+ ```typescript
39
+ z.object({
40
+ sent: z.boolean(),
41
+ messageId: z.string().optional(),
42
+ error: z.string().optional(),
43
+ })
44
+ ```
45
+
46
+ ---
47
+
48
+ ## Workflow Code Pattern
49
+
50
+ ```typescript
51
+ import type { WorkflowDefinition } from '@elevasis/sdk'
52
+ import { platform, PlatformToolError } from '@elevasis/sdk/worker'
53
+ import { z } from 'zod'
54
+
55
+ const inputSchema = z.object({
56
+ to: z.union([z.string(), z.array(z.string())]),
57
+ subject: z.string(),
58
+ html: z.string().optional(),
59
+ text: z.string().optional(),
60
+ from: z.string().optional(),
61
+ replyTo: z.string().optional(),
62
+ })
63
+ const outputSchema = z.object({
64
+ sent: z.boolean(),
65
+ messageId: z.string().optional(),
66
+ error: z.string().optional(),
67
+ })
68
+
69
+ type Input = z.infer<typeof inputSchema>
70
+
71
+ export const emailSender: WorkflowDefinition = {
72
+ config: {
73
+ resourceId: 'email-sender',
74
+ name: 'Email Sender',
75
+ type: 'workflow',
76
+ description: 'Sends transactional email via Resend',
77
+ version: '1.0.0',
78
+ status: 'dev',
79
+ },
80
+ contract: { inputSchema, outputSchema },
81
+ steps: {
82
+ send: {
83
+ id: 'send',
84
+ name: 'Send Email',
85
+ description: 'Send the email via Resend',
86
+ inputSchema,
87
+ outputSchema,
88
+ handler: async (input, context) => {
89
+ const { to, subject, html, text, from, replyTo } = input as Input
90
+
91
+ try {
92
+ const result = await platform.call({
93
+ tool: 'resend',
94
+ method: 'sendEmail',
95
+ credential: 'my-resend',
96
+ params: {
97
+ to: Array.isArray(to) ? to : [to],
98
+ subject,
99
+ html: html ?? `<p>${text ?? ''}</p>`,
100
+ text,
101
+ from: from ?? 'noreply@yourdomain.com',
102
+ replyTo,
103
+ },
104
+ }) as { id: string }
105
+
106
+ context.logger.info('Email sent', { messageId: result.id, to })
107
+ return { sent: true, messageId: result.id }
108
+ } catch (err) {
109
+ if (err instanceof PlatformToolError) {
110
+ context.logger.error('Email send failed', { error: err.message, to })
111
+ return { sent: false, error: err.message }
112
+ }
113
+ throw err
114
+ }
115
+ },
116
+ next: null,
117
+ },
118
+ },
119
+ entryPoint: 'send',
120
+ }
121
+ ```
122
+
123
+ ---
124
+
125
+ ## Adaptation Notes
126
+
127
+ - **Credential name:** Replace `'my-resend'` with the user's Resend credential name.
128
+ - **From address:** Replace `'noreply@yourdomain.com'` with the user's verified sender address (must be verified in Resend).
129
+ - **HTML vs text:** For beginners, suggest writing plain text first and adding HTML later. For developers, show both.
130
+ - **Error handling:** The template returns `{ sent: false, error }` on failure rather than throwing, suitable for non-critical emails. For critical notifications, change to `throw err` so the execution fails visibly.
131
+ - **Multi-step context:** This template is a single step, but it can be used as the last step of a multi-step workflow (e.g., "process data then send summary email").
132
+
133
+ ---
134
+
135
+ **Last Updated:** 2026-02-26
@@ -0,0 +1,175 @@
1
+ ---
2
+ title: "Template: Lead Scorer"
3
+ description: "LLM-based lead scoring with Supabase storage -- receive a lead, score it with an LLM, store the result"
4
+ loadWhen: "Applying the lead-scorer workflow template"
5
+ ---
6
+
7
+ **Category:** CRM
8
+
9
+ **Platform Tools:** `llm` (scoring), `supabase` (store score)
10
+
11
+ **Credentials Required:**
12
+
13
+ - `my-database` -- Supabase project URL and service role key
14
+ - LLM API keys are resolved server-side from platform configuration (no credential name needed)
15
+
16
+ ---
17
+
18
+ ## What This Workflow Does
19
+
20
+ Receives a lead with company and contact details, uses an LLM to score the lead on multiple criteria, and stores the scored lead in a Supabase table. The scoring criteria and output fields are customizable. Suitable for inbound lead qualification, prioritization queues, and sales routing.
21
+
22
+ ---
23
+
24
+ ## Input Schema
25
+
26
+ ```typescript
27
+ z.object({
28
+ leadId: z.string(),
29
+ company: z.string(),
30
+ role: z.string().optional(),
31
+ email: z.string().optional(),
32
+ notes: z.string().optional(), // Any additional context about the lead
33
+ scoringCriteria: z.string().optional(), // Custom scoring criteria (overrides default)
34
+ })
35
+ ```
36
+
37
+ ## Output Schema
38
+
39
+ ```typescript
40
+ z.object({
41
+ leadId: z.string(),
42
+ score: z.number(), // 0-100 overall score
43
+ tier: z.enum(['hot', 'warm', 'cold']),
44
+ reasoning: z.string(), // LLM explanation of the score
45
+ stored: z.boolean(),
46
+ })
47
+ ```
48
+
49
+ ---
50
+
51
+ ## Workflow Code Pattern
52
+
53
+ ```typescript
54
+ import type { WorkflowDefinition } from '@elevasis/sdk'
55
+ import { StepType } from '@elevasis/sdk'
56
+ import { platform } from '@elevasis/sdk/worker'
57
+ import { z } from 'zod'
58
+
59
+ const inputSchema = z.object({
60
+ leadId: z.string(),
61
+ company: z.string(),
62
+ role: z.string().optional(),
63
+ email: z.string().optional(),
64
+ notes: z.string().optional(),
65
+ scoringCriteria: z.string().optional(),
66
+ })
67
+ const outputSchema = z.object({
68
+ leadId: z.string(),
69
+ score: z.number(),
70
+ tier: z.enum(['hot', 'warm', 'cold']),
71
+ reasoning: z.string(),
72
+ stored: z.boolean(),
73
+ })
74
+
75
+ const DEFAULT_CRITERIA = `
76
+ Score this lead from 0-100 based on:
77
+ - Company fit (industry, size, budget signals) -- 40 points
78
+ - Role relevance (decision-maker or influencer) -- 30 points
79
+ - Engagement potential (any signals from notes) -- 30 points
80
+
81
+ Return JSON: { "score": number, "tier": "hot"|"warm"|"cold", "reasoning": "..." }
82
+ Hot = 70+, Warm = 40-69, Cold = below 40.
83
+ `
84
+
85
+ type Input = z.infer<typeof inputSchema>
86
+
87
+ export const leadScorer: WorkflowDefinition = {
88
+ config: {
89
+ resourceId: 'lead-scorer',
90
+ name: 'Lead Scorer',
91
+ type: 'workflow',
92
+ description: 'Scores leads using an LLM and stores results in Supabase',
93
+ version: '1.0.0',
94
+ status: 'dev',
95
+ },
96
+ contract: { inputSchema, outputSchema },
97
+ steps: {
98
+ score: {
99
+ id: 'score',
100
+ name: 'Score Lead',
101
+ description: 'Score the lead using an LLM',
102
+ inputSchema,
103
+ outputSchema: z.object({ leadId: z.string(), score: z.number(), tier: z.enum(['hot', 'warm', 'cold']), reasoning: z.string() }),
104
+ handler: async (input) => {
105
+ const { leadId, company, role, notes, scoringCriteria } = input as Input
106
+ const criteria = scoringCriteria ?? DEFAULT_CRITERIA
107
+
108
+ const result = await platform.call({
109
+ tool: 'llm',
110
+ method: 'generate',
111
+ params: {
112
+ provider: 'openai',
113
+ model: 'gpt-4o-mini',
114
+ messages: [{
115
+ role: 'user',
116
+ content: `${criteria}\n\nLead:\nCompany: ${company}\nRole: ${role ?? 'Unknown'}\nNotes: ${notes ?? 'None'}`,
117
+ }],
118
+ responseSchema: {
119
+ type: 'object',
120
+ properties: {
121
+ score: { type: 'number' },
122
+ tier: { type: 'string', enum: ['hot', 'warm', 'cold'] },
123
+ reasoning: { type: 'string' },
124
+ },
125
+ },
126
+ },
127
+ }) as { score: number; tier: 'hot' | 'warm' | 'cold'; reasoning: string }
128
+
129
+ return { leadId, score: result.score, tier: result.tier, reasoning: result.reasoning }
130
+ },
131
+ next: { type: StepType.LINEAR, target: 'store' },
132
+ },
133
+ store: {
134
+ id: 'store',
135
+ name: 'Store Score',
136
+ description: 'Save the lead score to Supabase',
137
+ inputSchema: z.object({ leadId: z.string(), score: z.number(), tier: z.enum(['hot', 'warm', 'cold']), reasoning: z.string() }),
138
+ outputSchema,
139
+ handler: async (input, context) => {
140
+ const { leadId, score, tier, reasoning } = input as { leadId: string; score: number; tier: 'hot' | 'warm' | 'cold'; reasoning: string }
141
+
142
+ await platform.call({
143
+ tool: 'supabase',
144
+ method: 'update',
145
+ credential: 'my-database',
146
+ params: {
147
+ table: 'leads',
148
+ filter: { id: `eq.${leadId}` },
149
+ data: { score, tier, scoring_reasoning: reasoning, scored_at: new Date().toISOString() },
150
+ },
151
+ })
152
+
153
+ context.logger.info('Lead scored and stored', { leadId, score, tier })
154
+ return { leadId, score, tier, reasoning, stored: true }
155
+ },
156
+ next: null,
157
+ },
158
+ },
159
+ entryPoint: 'score',
160
+ }
161
+ ```
162
+
163
+ ---
164
+
165
+ ## Adaptation Notes
166
+
167
+ - **Credential name:** Replace `'my-database'` with the user's database credential name.
168
+ - **Table name:** Replace `'leads'` with the user's actual table name. Check `data/schema.ts` if it exists.
169
+ - **Scoring criteria:** Customize `DEFAULT_CRITERIA` based on the user's ICP (ideal customer profile). Ask the user what matters most for their scoring.
170
+ - **Table columns:** The template writes `score`, `tier`, `scoring_reasoning`, `scored_at`. Ensure these columns exist or adapt the field names.
171
+ - **LLM model:** Default uses `gpt-4o-mini`. For more nuanced scoring, suggest `gpt-4o`.
172
+
173
+ ---
174
+
175
+ **Last Updated:** 2026-02-26
@@ -0,0 +1,151 @@
1
+ ---
2
+ title: "Template: PDF Generator"
3
+ description: "PDF generation from structured data with platform storage upload -- render a PDF from a template and upload to platform storage"
4
+ loadWhen: "Applying the pdf-generator workflow template"
5
+ ---
6
+
7
+ **Category:** Documents
8
+
9
+ **Platform Tools:** `pdf` (render), `storage` (upload)
10
+
11
+ **Credentials Required:**
12
+
13
+ - None required (pdf and storage are platform services, not integrations)
14
+
15
+ ---
16
+
17
+ ## What This Workflow Does
18
+
19
+ Receives structured data, renders a PDF using an HTML template, uploads the result to platform storage, and returns a signed URL for download. Suitable for invoices, reports, certificates, contracts, and any document that needs to be generated on demand and delivered as a downloadable file.
20
+
21
+ ---
22
+
23
+ ## Input Schema
24
+
25
+ ```typescript
26
+ z.object({
27
+ templateHtml: z.string(), // HTML template with {variable} placeholders
28
+ data: z.record(z.unknown()), // Data to inject into the template
29
+ filename: z.string(), // Output filename (without .pdf extension)
30
+ expiresInSeconds: z.number().optional(), // Signed URL expiry (default: 3600)
31
+ })
32
+ ```
33
+
34
+ ## Output Schema
35
+
36
+ ```typescript
37
+ z.object({
38
+ downloadUrl: z.string(), // Signed URL for the generated PDF
39
+ storageKey: z.string(), // Storage key for future reference
40
+ expiresAt: z.string(), // ISO timestamp when URL expires
41
+ })
42
+ ```
43
+
44
+ ---
45
+
46
+ ## Workflow Code Pattern
47
+
48
+ ```typescript
49
+ import type { WorkflowDefinition } from '@elevasis/sdk'
50
+ import { StepType } from '@elevasis/sdk'
51
+ import { platform } from '@elevasis/sdk/worker'
52
+ import { z } from 'zod'
53
+
54
+ const inputSchema = z.object({
55
+ templateHtml: z.string(),
56
+ data: z.record(z.unknown()),
57
+ filename: z.string(),
58
+ expiresInSeconds: z.number().optional(),
59
+ })
60
+ const outputSchema = z.object({
61
+ downloadUrl: z.string(),
62
+ storageKey: z.string(),
63
+ expiresAt: z.string(),
64
+ })
65
+
66
+ type Input = z.infer<typeof inputSchema>
67
+
68
+ function renderTemplate(html: string, data: Record<string, unknown>): string {
69
+ return html.replace(/\{(\w+)\}/g, (_, key) => String(data[key] ?? ''))
70
+ }
71
+
72
+ export const pdfGenerator: WorkflowDefinition = {
73
+ config: {
74
+ resourceId: 'pdf-generator',
75
+ name: 'PDF Generator',
76
+ type: 'workflow',
77
+ description: 'Generates a PDF from HTML template and uploads to storage',
78
+ version: '1.0.0',
79
+ status: 'dev',
80
+ },
81
+ contract: { inputSchema, outputSchema },
82
+ steps: {
83
+ render: {
84
+ id: 'render',
85
+ name: 'Render PDF',
86
+ description: 'Render HTML template to PDF buffer',
87
+ inputSchema,
88
+ outputSchema: z.object({ buffer: z.string(), filename: z.string(), expiresInSeconds: z.number() }),
89
+ handler: async (input) => {
90
+ const { templateHtml, data, filename, expiresInSeconds } = input as Input
91
+ const html = renderTemplate(templateHtml, data)
92
+
93
+ const result = await platform.call({
94
+ tool: 'pdf',
95
+ method: 'renderToBuffer',
96
+ params: { html },
97
+ }) as { buffer: string } // base64-encoded buffer
98
+
99
+ return { buffer: result.buffer, filename, expiresInSeconds: expiresInSeconds ?? 3600 }
100
+ },
101
+ next: { type: StepType.LINEAR, target: 'upload' },
102
+ },
103
+ upload: {
104
+ id: 'upload',
105
+ name: 'Upload PDF',
106
+ description: 'Upload rendered PDF to platform storage and get signed URL',
107
+ inputSchema: z.object({ buffer: z.string(), filename: z.string(), expiresInSeconds: z.number() }),
108
+ outputSchema,
109
+ handler: async (input, context) => {
110
+ const { buffer, filename, expiresInSeconds } = input as { buffer: string; filename: string; expiresInSeconds: number }
111
+ const key = `documents/${filename}-${Date.now()}.pdf`
112
+
113
+ await platform.call({
114
+ tool: 'storage',
115
+ method: 'upload',
116
+ params: {
117
+ key,
118
+ content: buffer,
119
+ contentType: 'application/pdf',
120
+ },
121
+ })
122
+
123
+ const urlResult = await platform.call({
124
+ tool: 'storage',
125
+ method: 'createSignedUrl',
126
+ params: { key, expiresIn: expiresInSeconds },
127
+ }) as { signedUrl: string; expiresAt: string }
128
+
129
+ context.logger.info('PDF generated and uploaded', { key, filename })
130
+ return { downloadUrl: urlResult.signedUrl, storageKey: key, expiresAt: urlResult.expiresAt }
131
+ },
132
+ next: null,
133
+ },
134
+ },
135
+ entryPoint: 'render',
136
+ }
137
+ ```
138
+
139
+ ---
140
+
141
+ ## Adaptation Notes
142
+
143
+ - **Template format:** The simple `{variable}` placeholder system handles most cases. For complex documents, suggest the user write a more complete HTML template with inline CSS.
144
+ - **Filename:** The template appends a timestamp to avoid collisions. Adapt naming to the user's convention (e.g., `invoice-{invoiceId}`).
145
+ - **Expiry:** Default is 1 hour. For document delivery workflows, increase to 24-72 hours.
146
+ - **Data structure:** Ask the user what fields their document needs before defining the `data` schema. For typed use cases (invoices, reports), define a stricter input schema.
147
+ - **Skill adaptation:** For beginners, show a complete example HTML template and explain what "template variables" are.
148
+
149
+ ---
150
+
151
+ **Last Updated:** 2026-02-26
@@ -0,0 +1,189 @@
1
+ ---
2
+ title: "Template: Recurring Job"
3
+ description: "Scheduler-triggered periodic workflow -- run a task on a schedule (daily, weekly, hourly, or custom cron)"
4
+ loadWhen: "Applying the recurring-job workflow template"
5
+ ---
6
+
7
+ **Category:** Scheduling
8
+
9
+ **Platform Tools:** `scheduler` (create/manage schedule)
10
+
11
+ **Credentials Required:**
12
+
13
+ - None required (scheduler is a platform service, not an integration)
14
+
15
+ ---
16
+
17
+ ## What This Workflow Does
18
+
19
+ Two-part pattern for recurring jobs:
20
+
21
+ 1. **Setup workflow** (`recurring-job-setup`): Creates a schedule entry that triggers the main job workflow on a recurring basis. Run once to activate.
22
+ 2. **Main job workflow** (`recurring-job`): The actual work executed on each scheduled trigger.
23
+
24
+ The job workflow and setup workflow share a schedule key for idempotent schedule management.
25
+
26
+ ---
27
+
28
+ ## Input Schema (Setup)
29
+
30
+ ```typescript
31
+ z.object({
32
+ cronExpression: z.string(), // Cron expression (e.g., '0 9 * * 1-5' for weekdays at 9am)
33
+ timezone: z.string().optional(), // Timezone (default: 'UTC')
34
+ jobInput: z.record(z.unknown()).optional(), // Input to pass to each job execution
35
+ })
36
+ ```
37
+
38
+ ## Input Schema (Job)
39
+
40
+ ```typescript
41
+ z.object({
42
+ scheduledAt: z.string(), // ISO timestamp of the scheduled trigger
43
+ jobInput: z.record(z.unknown()).optional(), // Passed through from schedule creation
44
+ })
45
+ ```
46
+
47
+ ## Output Schema (Job)
48
+
49
+ ```typescript
50
+ z.object({
51
+ completed: z.boolean(),
52
+ processedAt: z.string(),
53
+ summary: z.string(),
54
+ })
55
+ ```
56
+
57
+ ---
58
+
59
+ ## Workflow Code Pattern
60
+
61
+ ```typescript
62
+ import type { WorkflowDefinition } from '@elevasis/sdk'
63
+ import { platform } from '@elevasis/sdk/worker'
64
+ import { z } from 'zod'
65
+
66
+ // Setup workflow -- run once to create the schedule
67
+ export const recurringJobSetup: WorkflowDefinition = {
68
+ config: {
69
+ resourceId: 'recurring-job-setup',
70
+ name: 'Recurring Job Setup',
71
+ type: 'workflow',
72
+ description: 'Creates or updates the schedule for the recurring job',
73
+ version: '1.0.0',
74
+ status: 'dev',
75
+ },
76
+ contract: {
77
+ inputSchema: z.object({
78
+ cronExpression: z.string(),
79
+ timezone: z.string().optional(),
80
+ jobInput: z.record(z.unknown()).optional(),
81
+ }),
82
+ outputSchema: z.object({
83
+ scheduleId: z.string(),
84
+ active: z.boolean(),
85
+ }),
86
+ },
87
+ steps: {
88
+ createSchedule: {
89
+ id: 'createSchedule',
90
+ name: 'Create Schedule',
91
+ description: 'Set up the recurring schedule',
92
+ inputSchema: z.object({ cronExpression: z.string(), timezone: z.string().optional(), jobInput: z.record(z.unknown()).optional() }),
93
+ outputSchema: z.object({ scheduleId: z.string(), active: z.boolean() }),
94
+ handler: async (input, context) => {
95
+ const { cronExpression, timezone, jobInput } = input as { cronExpression: string; timezone?: string; jobInput?: Record<string, unknown> }
96
+
97
+ const result = await platform.call({
98
+ tool: 'scheduler',
99
+ method: 'createSchedule',
100
+ params: {
101
+ resourceId: 'recurring-job',
102
+ cronExpression,
103
+ timezone: timezone ?? 'UTC',
104
+ input: { scheduledAt: new Date().toISOString(), jobInput: jobInput ?? {} },
105
+ idempotencyKey: 'recurring-job-schedule',
106
+ },
107
+ }) as { id: string }
108
+
109
+ context.logger.info('Schedule created', { scheduleId: result.id, cronExpression })
110
+ return { scheduleId: result.id, active: true }
111
+ },
112
+ next: null,
113
+ },
114
+ },
115
+ entryPoint: 'createSchedule',
116
+ }
117
+
118
+ // Main job workflow -- triggered by the scheduler on each run
119
+ export const recurringJob: WorkflowDefinition = {
120
+ config: {
121
+ resourceId: 'recurring-job',
122
+ name: 'Recurring Job',
123
+ type: 'workflow',
124
+ description: 'Executes on each scheduled trigger',
125
+ version: '1.0.0',
126
+ status: 'dev',
127
+ },
128
+ contract: {
129
+ inputSchema: z.object({
130
+ scheduledAt: z.string(),
131
+ jobInput: z.record(z.unknown()).optional(),
132
+ }),
133
+ outputSchema: z.object({
134
+ completed: z.boolean(),
135
+ processedAt: z.string(),
136
+ summary: z.string(),
137
+ }),
138
+ },
139
+ steps: {
140
+ run: {
141
+ id: 'run',
142
+ name: 'Run Job',
143
+ description: 'Execute the recurring job logic',
144
+ inputSchema: z.object({ scheduledAt: z.string(), jobInput: z.record(z.unknown()).optional() }),
145
+ outputSchema: z.object({ completed: z.boolean(), processedAt: z.string(), summary: z.string() }),
146
+ handler: async (input, context) => {
147
+ const { scheduledAt, jobInput } = input as { scheduledAt: string; jobInput?: Record<string, unknown> }
148
+
149
+ context.logger.info('Recurring job started', { scheduledAt, jobInput })
150
+
151
+ // === REPLACE THIS SECTION WITH THE ACTUAL JOB LOGIC ===
152
+ // Example: fetch data, process it, send a report
153
+ const summary = `Job completed at ${new Date().toISOString()}`
154
+ // ======================================================
155
+
156
+ return { completed: true, processedAt: new Date().toISOString(), summary }
157
+ },
158
+ next: null,
159
+ },
160
+ },
161
+ entryPoint: 'run',
162
+ }
163
+ ```
164
+
165
+ ---
166
+
167
+ ## Common Cron Expressions
168
+
169
+ | Schedule | Expression | Notes |
170
+ | --- | --- | --- |
171
+ | Every day at 9am (weekdays) | `0 9 * * 1-5` | Monday-Friday |
172
+ | Every day at midnight | `0 0 * * *` | UTC unless timezone set |
173
+ | Every hour | `0 * * * *` | On the hour |
174
+ | Every Monday at 8am | `0 8 * * 1` | |
175
+ | First of each month | `0 0 1 * *` | Midnight UTC |
176
+ | Every 15 minutes | `*/15 * * * *` | Frequent polling |
177
+
178
+ ---
179
+
180
+ ## Adaptation Notes
181
+
182
+ - **Job logic:** Replace the placeholder comment in the `run` handler with the actual job logic. Ask the user what the job should do before generating the full workflow.
183
+ - **Both workflows needed:** Remind the user to add both `recurringJob` and `recurringJobSetup` to their `src/index.ts` registry. Run `setup` once to activate; only `recurring-job` runs automatically thereafter.
184
+ - **Idempotency key:** The `recurring-job-schedule` key ensures re-running setup does not create duplicate schedules.
185
+ - **Timezone:** Always ask the user their preferred timezone. Defaults to UTC which may cause unexpected run times.
186
+
187
+ ---
188
+
189
+ **Last Updated:** 2026-02-26