@elevasis/sdk 1.38.0 → 1.40.0

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.
@@ -1,415 +1,415 @@
1
- ---
2
- title: Workflow Recipes
3
- description: Anatomy of a workflow, adapter usage, and trigger patterns -- runnable email-notification example replaces the trivial echo workflow. Resolves eval score 2/5 on workflow authoring.
4
- ---
1
+ ---
2
+ title: Workflow Recipes
3
+ description: Anatomy of a workflow, adapter usage, and trigger patterns -- runnable email-notification example replaces the trivial echo workflow. Resolves eval score 2/5 on workflow authoring.
4
+ ---
5
5
  <!-- @generated by packages/sdk/scripts/copy-reference-docs.mjs -- DO NOT EDIT -->
6
6
  <!-- Regenerate: pnpm scaffold:sync -->
7
7
 
8
-
9
- # Workflow Recipes
10
-
11
- `🟢 Stable` -- Use these patterns as-is against `@elevasis/sdk ^1.4.0`.
12
-
13
- ---
14
-
15
- ## 1. Anatomy of a Workflow
16
-
17
- Every workflow is a `WorkflowDefinition` object with four top-level keys: `config`, `contract`, `steps`, and `entryPoint`. The `email-notification` workflow at `operations/src/email-notification/index.ts` is used as the running example throughout this section.
18
-
19
- ### Config
20
-
21
- ```typescript
22
- import { resourceDescriptors } from '@core/config/organization-model'
23
-
24
- config: {
25
- resource: resourceDescriptors.emailNotification,
26
- resourceId: resourceDescriptors.emailNotification.id,
27
- name: 'Email Notification',
28
- type: resourceDescriptors.emailNotification.kind,
29
- description: 'Sends a notification email to a user...',
30
- version: '1.0.0',
31
- status: 'dev'
32
- }
33
- ```
34
-
35
- - The OM Resources descriptor owns the stable ID and kind. Runtime execution still uses that deployed `resourceId`, but workflow authoring should derive it from the descriptor.
36
- - Bump `version` whenever you change `contract.inputSchema` or `contract.outputSchema`.
37
-
38
- ### Contract
39
-
40
- ```typescript
41
- // core/types/index.ts -- shared with frontend
42
- export const emailNotificationInputSchema = z.object({
43
- recipientEmail: z.string().email(),
44
- recipientName: z.string().min(1),
45
- subject: z.string().min(1).max(200),
46
- body: z.string().min(1).max(10000),
47
- category: z.string().min(1).default('operations'),
48
- actionUrl: z.string().url().optional()
49
- })
50
-
51
- export const emailNotificationOutputSchema = z.object({
52
- delivered: z.boolean(),
53
- notificationId: z.string().optional(),
54
- summary: z.string()
55
- })
56
-
57
- export type EmailNotificationInput = z.infer<typeof emailNotificationInputSchema>
58
- export type EmailNotificationOutput = z.infer<typeof emailNotificationOutputSchema>
59
- ```
60
-
61
- ```typescript
62
- // operations/src/email-notification/index.ts
63
- import { emailNotificationInputSchema, emailNotificationOutputSchema } from '@core/types'
64
-
65
- contract: {
66
- inputSchema: emailNotificationInputSchema,
67
- outputSchema: emailNotificationOutputSchema
68
- }
69
- ```
70
-
71
- **Rules:**
72
-
73
- - Define schemas in `core/types/index.ts` -- never inline Zod schemas in workflow files.
74
- - Both runtimes (frontend and platform) import from `@core/types`, keeping validation consistent.
75
- - `@core/types` resolves to `core/types/index.ts` via the tsconfig path alias.
76
-
77
- **Entity-backed workflows:** for workflows that operate on a domain entity, reference the entity contract rather than redeclaring it.
78
-
79
- ```typescript
80
- // core/types/index.ts
81
- import { BaseDealSchema } from '@elevasis/core/entities'
82
- import { DealSchema } from '@core/types/entities' // project-local extended version
83
-
84
- export const closeDealInputSchema = z.object({
85
- deal: DealSchema,
86
- closedReason: z.string()
87
- })
88
-
89
- export type CloseDealInput = z.infer<typeof closeDealInputSchema>
90
- ```
91
-
92
- `DealSchema` is defined in `core/types/entities.ts` by extending `BaseDealSchema` with project-specific metadata. See [../recipes/extend-a-base-entity.md](../recipes/extend-a-base-entity.md) for the pattern.
93
-
94
- ### Steps
95
-
96
- Each step is a `WorkflowStep` with: `id`, `name`, `description`, `handler`, `inputSchema`, `outputSchema`, and `next`.
97
-
98
- ```typescript
99
- import { StepType } from '@elevasis/sdk'
100
-
101
- steps: {
102
- validate: {
103
- id: 'validate',
104
- name: 'Validate Input',
105
- description: 'Validates recipient and content parameters before sending',
106
- handler: async (rawInput, context) => {
107
- const input = rawInput as EmailNotificationInput
108
- context.logger.info(`[validate] Checking recipient: ${input.recipientEmail}`)
109
- // ... validation logic ...
110
- return input // Pass validated data to next step
111
- },
112
- inputSchema: emailNotificationInputSchema,
113
- outputSchema: emailNotificationInputSchema, // Passes full input through to next step
114
- next: { type: StepType.LINEAR, target: 'notify' }
115
- },
116
- notify: {
117
- id: 'notify',
118
- name: 'Send Notification',
119
- handler: async (rawInput, context) => {
120
- // ...
121
- return { delivered: true, summary: '...' }
122
- },
123
- inputSchema: emailNotificationInputSchema,
124
- outputSchema: emailNotificationOutputSchema,
125
- next: null // Terminal step -- ends the workflow
126
- }
127
- }
128
- ```
129
-
130
- **Key rules:**
131
-
132
- - `next: null` marks a terminal step.
133
- - `next: { type: StepType.LINEAR, target: 'stepId' }` chains to another step.
134
- - Use `context.logger` -- never `console.log`. Platform captures `context.logger.*` only.
135
- - Cast `rawInput` to your input type: `const input = rawInput as EmailNotificationInput`.
136
-
137
- ### Entrypoint
138
-
139
- ```typescript
140
- entryPoint: 'validate' // Must match a step id in the steps map
141
- ```
142
-
143
- The platform starts execution here. For single-step workflows, `entryPoint` points to the only step (e.g., `echo` points to `'echo'`).
144
-
145
- ### Optional: Interface Form
146
-
147
- Declare `interface.form` to auto-generate an execution form in AI Studio and Command Center:
148
-
149
- ```typescript
150
- interface: {
151
- form: {
152
- title: 'Send Email Notification',
153
- description: 'Sends a notification email to a user.',
154
- fields: [
155
- { name: 'recipientEmail', label: 'Recipient Email', type: 'text', required: true },
156
- { name: 'body', label: 'Body', type: 'text', required: true }
157
- ],
158
- submitButton: { label: 'Send notification', loadingLabel: 'Sending...' }
159
- }
160
- }
161
- ```
162
-
163
- ---
164
-
165
- ## 2. Adapter Usage
166
-
167
- Adapters are typed wrappers over `platform.call()`. Import singletons from `@elevasis/sdk/worker`.
168
-
169
- ### notifications
170
-
171
- Send a platform notification to the current user. `userId` and `organizationId` are injected server-side -- you never supply them.
172
-
173
- ```typescript
174
- import { notifications } from '@elevasis/sdk/worker'
175
-
176
- await notifications.create({
177
- category: 'operations', // Any string: 'operations', 'delivery', 'acquisition', etc.
178
- title: 'Workflow complete',
179
- message: 'Your email notification was sent successfully.',
180
- actionUrl: '/operations' // Optional -- link the user can follow
181
- })
182
- ```
183
-
184
- Available methods: `create`.
185
-
186
- ### llm
187
-
188
- Generate text or structured output using a language model.
189
-
190
- ```typescript
191
- import { llm } from '@elevasis/sdk/worker'
192
-
193
- const response = await llm.generate({
194
- provider: 'anthropic', // 'anthropic' | 'openai' | 'openrouter' | 'google'
195
- model: 'claude-sonnet-4-5',
196
- messages: [
197
- { role: 'user', content: 'Summarize this email body: ' + input.body }
198
- ]
199
- })
200
-
201
- const summary = response.output as string
202
- ```
203
-
204
- For structured output, pass a JSON Schema as `responseSchema`:
205
-
206
- ```typescript
207
- const response = await llm.generate({
208
- provider: 'anthropic',
209
- model: 'claude-sonnet-4-5',
210
- messages: [{ role: 'user', content: 'Extract the key action from: ' + input.body }],
211
- responseSchema: {
212
- type: 'object',
213
- properties: { action: { type: 'string' }, urgency: { type: 'string' } }
214
- }
215
- })
216
- const { action, urgency } = response.output as { action: string; urgency: string }
217
- ```
218
-
219
- Available methods: `generate`.
220
-
221
- ### storage
222
-
223
- Upload and retrieve files scoped to the organization. All paths are automatically prefixed with the organization's storage prefix server-side.
224
-
225
- ```typescript
226
- import { storage } from '@elevasis/sdk/worker'
227
-
228
- // Upload
229
- await storage.upload({
230
- bucket: 'operations',
231
- path: 'email-logs/2026-04-16/batch-01.json',
232
- content: Buffer.from(JSON.stringify(logData)).toString('base64'),
233
- contentType: 'application/json'
234
- })
235
-
236
- // Download
237
- const file = await storage.download({
238
- bucket: 'operations',
239
- path: 'email-logs/2026-04-16/batch-01.json'
240
- })
241
-
242
- // Signed URL for frontend access
243
- const { signedUrl } = await storage.createSignedUrl({
244
- bucket: 'operations',
245
- path: 'email-logs/2026-04-16/batch-01.json',
246
- expiresIn: 3600
247
- })
248
- ```
249
-
250
- Available methods: `upload`, `download`, `createSignedUrl`, `delete`, `list`.
251
-
252
- ### scheduler
253
-
254
- Schedule future or recurring workflow executions.
255
-
256
- ```typescript
257
- import { scheduler } from '@elevasis/sdk/worker'
258
-
259
- const schedule = await scheduler.createSchedule({
260
- name: 'Daily email digest',
261
- target: { resourceType: 'workflow', resourceId: 'email-notification' },
262
- scheduleConfig: {
263
- type: 'cron',
264
- expression: '0 9 * * 1-5' // 9am weekdays
265
- }
266
- })
267
-
268
- context.logger.info(`[schedule] Created schedule: ${schedule.id}`)
269
- ```
270
-
271
- Available methods: `createSchedule`, `updateAnchor`, `deleteSchedule`, `getSchedule`, `listSchedules`, `cancelSchedule`, `cancelSchedulesByMetadata`, `cancelScheduleByIdempotencyKey`, `findByIdempotencyKey`, `deleteScheduleByIdempotencyKey`.
272
-
273
- **Note on other adapters:** Integration adapters (`createResendAdapter`, `createAttioAdapter`, etc.) follow a factory pattern -- bind a credential once, use the instance for all calls:
274
-
275
- ```typescript
276
- import { createResendAdapter } from '@elevasis/sdk/worker'
277
-
278
- const resend = createResendAdapter('my-resend-credential')
279
- await resend.sendEmail({
280
- to: input.recipientEmail,
281
- subject: input.subject,
282
- html: `<p>${input.body}</p>`
283
- })
284
- ```
285
-
286
- See `operations/node_modules/@elevasis/sdk/reference/` for the full adapter reference.
287
-
288
- ---
289
-
290
- ## 3. Trigger Patterns from Frontend
291
-
292
- ### (a) API call via `useApiClient`
293
-
294
- Use this pattern in React components and hooks. `apiRequest` automatically attaches the auth token and org context.
295
-
296
- ```typescript
297
- // ui/src/features/notifications/hooks/useSendEmailNotification.ts
298
- import { useMutation } from '@tanstack/react-query'
299
- import { useApiClient } from '@/lib/hooks/useApiClient'
300
- import type { EmailNotificationInput, EmailNotificationOutput } from '@core/types'
301
-
302
- export function useSendEmailNotification() {
303
- const { apiRequest } = useApiClient()
304
-
305
- return useMutation({
306
- mutationFn: async (input: EmailNotificationInput) => {
307
- return apiRequest<EmailNotificationOutput>('/execute', {
308
- method: 'POST',
309
- body: JSON.stringify({
310
- resourceType: 'workflow',
311
- resourceId: 'email-notification',
312
- input
313
- })
314
- })
315
- }
316
- })
317
- }
318
- ```
319
-
320
- Usage in a component:
321
-
322
- ```tsx
323
- // ui/src/features/notifications/components/SendNotificationButton.tsx
324
- import { useSendEmailNotification } from '../hooks/useSendEmailNotification'
325
-
326
- function SendNotificationButton() {
327
- const { mutate, isPending, isSuccess } = useSendEmailNotification()
328
-
329
- return (
330
- <Button
331
- loading={isPending}
332
- onClick={() =>
333
- mutate({
334
- recipientEmail: 'user@example.com',
335
- recipientName: 'Jane Smith',
336
- subject: 'Your request is ready',
337
- body: 'Hi Jane, your workflow has completed.',
338
- category: 'operations'
339
- })
340
- }
341
- >
342
- Send Notification
343
- </Button>
344
- )
345
- }
346
- ```
347
-
348
- For async execution (long-running workflows), use `/execute-async` instead:
349
-
350
- ```typescript
351
- return apiRequest<{ executionId: string }>('/execute-async', {
352
- method: 'POST',
353
- body: JSON.stringify({
354
- resourceType: 'workflow',
355
- resourceId: 'email-notification',
356
- input
357
- })
358
- })
359
- // Poll /executions/email-notification/:executionId for status
360
- ```
361
-
362
- ### (b) Direct SDK dispatch (CLI or scripts)
363
-
364
- From the project root, use the platform CLI for manual invocations, testing, and scripting:
365
-
366
- ```bash
367
- # Describe the schema before executing
368
- pnpm exec elevasis describe Elevasis/email-notification
369
-
370
- # Execute synchronously
371
- pnpm exec elevasis exec Elevasis/email-notification --input '{
372
- "recipientEmail": "user@example.com",
373
- "recipientName": "Jane Smith",
374
- "subject": "Hello",
375
- "body": "Hi Jane, this is a test notification."
376
- }'
377
-
378
- # Execute asynchronously (for long-running workflows)
379
- pnpm exec elevasis exec Elevasis/email-notification --async --input '{...}'
380
-
381
- # View a specific execution
382
- pnpm exec elevasis execution Elevasis/email-notification <executionId>
383
- ```
384
-
385
- The `--prod` flag targets `https://api.elevasis.io` and goes **before** the command:
386
-
387
- ```bash
388
- pnpm exec elevasis --prod exec Elevasis/email-notification --input '{...}'
389
- ```
390
-
391
- ---
392
-
393
- ## 4. Registry Pattern
394
-
395
- Workflows are discovered through `operations/src/index.ts`, which exports a `DeploymentSpec` as its default export. The spec assembles deployable runtime resources; it is not a second resource identity catalog.
396
-
397
- **Pattern:** each feature group in `operations/src/` has its own exports barrel. The top-level spec spreads all groups:
398
-
399
- ```
400
- operations/src/
401
- index.ts # Top-level DeploymentSpec -- never add workflows here directly
402
- example/
403
- index.ts # export const workflows = [echo]; export const agents = []
404
- echo.ts # WorkflowDefinition for 'echo'
405
- email-notification/
406
- exports.ts # export const workflows = [emailNotification]; export const agents = []
407
- index.ts # WorkflowDefinition for 'email-notification'
408
- ```
409
-
410
- Top-level registry (`operations/src/index.ts`):
411
-
412
- ```typescript
8
+
9
+ # Workflow Recipes
10
+
11
+ `🟢 Stable` -- Use these patterns as-is against `@elevasis/sdk ^1.4.0`.
12
+
13
+ ---
14
+
15
+ ## 1. Anatomy of a Workflow
16
+
17
+ Every workflow is a `WorkflowDefinition` object with four top-level keys: `config`, `contract`, `steps`, and `entryPoint`. The `email-notification` workflow at `operations/src/email-notification/index.ts` is used as the running example throughout this section.
18
+
19
+ ### Config
20
+
21
+ ```typescript
22
+ import { resourceDescriptors } from '@core/config/organization-model'
23
+
24
+ config: {
25
+ resource: resourceDescriptors.emailNotification,
26
+ resourceId: resourceDescriptors.emailNotification.id,
27
+ name: 'Email Notification',
28
+ type: resourceDescriptors.emailNotification.kind,
29
+ description: 'Sends a notification email to a user...',
30
+ version: '1.0.0',
31
+ status: 'dev'
32
+ }
33
+ ```
34
+
35
+ - The OM Resources descriptor owns the stable ID and kind. Runtime execution still uses that deployed `resourceId`, but workflow authoring should derive it from the descriptor.
36
+ - Bump `version` whenever you change `contract.inputSchema` or `contract.outputSchema`.
37
+
38
+ ### Contract
39
+
40
+ ```typescript
41
+ // core/types/index.ts -- shared with frontend
42
+ export const emailNotificationInputSchema = z.object({
43
+ recipientEmail: z.string().email(),
44
+ recipientName: z.string().min(1),
45
+ subject: z.string().min(1).max(200),
46
+ body: z.string().min(1).max(10000),
47
+ category: z.string().min(1).default('operations'),
48
+ actionUrl: z.string().url().optional()
49
+ })
50
+
51
+ export const emailNotificationOutputSchema = z.object({
52
+ delivered: z.boolean(),
53
+ notificationId: z.string().optional(),
54
+ summary: z.string()
55
+ })
56
+
57
+ export type EmailNotificationInput = z.infer<typeof emailNotificationInputSchema>
58
+ export type EmailNotificationOutput = z.infer<typeof emailNotificationOutputSchema>
59
+ ```
60
+
61
+ ```typescript
62
+ // operations/src/email-notification/index.ts
63
+ import { emailNotificationInputSchema, emailNotificationOutputSchema } from '@core/types'
64
+
65
+ contract: {
66
+ inputSchema: emailNotificationInputSchema,
67
+ outputSchema: emailNotificationOutputSchema
68
+ }
69
+ ```
70
+
71
+ **Rules:**
72
+
73
+ - Define schemas in `core/types/index.ts` -- never inline Zod schemas in workflow files.
74
+ - Both runtimes (frontend and platform) import from `@core/types`, keeping validation consistent.
75
+ - `@core/types` resolves to `core/types/index.ts` via the tsconfig path alias.
76
+
77
+ **Entity-backed workflows:** for workflows that operate on a domain entity, reference the entity contract rather than redeclaring it.
78
+
79
+ ```typescript
80
+ // core/types/index.ts
81
+ import { BaseDealSchema } from '@elevasis/core/entities'
82
+ import { DealSchema } from '@core/types/entities' // project-local extended version
83
+
84
+ export const closeDealInputSchema = z.object({
85
+ deal: DealSchema,
86
+ closedReason: z.string()
87
+ })
88
+
89
+ export type CloseDealInput = z.infer<typeof closeDealInputSchema>
90
+ ```
91
+
92
+ `DealSchema` is defined in `core/types/entities.ts` by extending `BaseDealSchema` with project-specific metadata. See [../recipes/extend-a-base-entity.md](../recipes/extend-a-base-entity.md) for the pattern.
93
+
94
+ ### Steps
95
+
96
+ Each step is a `WorkflowStep` with: `id`, `name`, `description`, `handler`, `inputSchema`, `outputSchema`, and `next`.
97
+
98
+ ```typescript
99
+ import { StepType } from '@elevasis/sdk'
100
+
101
+ steps: {
102
+ validate: {
103
+ id: 'validate',
104
+ name: 'Validate Input',
105
+ description: 'Validates recipient and content parameters before sending',
106
+ handler: async (rawInput, context) => {
107
+ const input = rawInput as EmailNotificationInput
108
+ context.logger.info(`[validate] Checking recipient: ${input.recipientEmail}`)
109
+ // ... validation logic ...
110
+ return input // Pass validated data to next step
111
+ },
112
+ inputSchema: emailNotificationInputSchema,
113
+ outputSchema: emailNotificationInputSchema, // Passes full input through to next step
114
+ next: { type: StepType.LINEAR, target: 'notify' }
115
+ },
116
+ notify: {
117
+ id: 'notify',
118
+ name: 'Send Notification',
119
+ handler: async (rawInput, context) => {
120
+ // ...
121
+ return { delivered: true, summary: '...' }
122
+ },
123
+ inputSchema: emailNotificationInputSchema,
124
+ outputSchema: emailNotificationOutputSchema,
125
+ next: null // Terminal step -- ends the workflow
126
+ }
127
+ }
128
+ ```
129
+
130
+ **Key rules:**
131
+
132
+ - `next: null` marks a terminal step.
133
+ - `next: { type: StepType.LINEAR, target: 'stepId' }` chains to another step.
134
+ - Use `context.logger` -- never `console.log`. Platform captures `context.logger.*` only.
135
+ - Cast `rawInput` to your input type: `const input = rawInput as EmailNotificationInput`.
136
+
137
+ ### Entrypoint
138
+
139
+ ```typescript
140
+ entryPoint: 'validate' // Must match a step id in the steps map
141
+ ```
142
+
143
+ The platform starts execution here. For single-step workflows, `entryPoint` points to the only step (e.g., `echo` points to `'echo'`).
144
+
145
+ ### Optional: Interface Form
146
+
147
+ Declare `interface.form` to auto-generate an execution form in AI Studio and Command Center:
148
+
149
+ ```typescript
150
+ interface: {
151
+ form: {
152
+ title: 'Send Email Notification',
153
+ description: 'Sends a notification email to a user.',
154
+ fields: [
155
+ { name: 'recipientEmail', label: 'Recipient Email', type: 'text', required: true },
156
+ { name: 'body', label: 'Body', type: 'text', required: true }
157
+ ],
158
+ submitButton: { label: 'Send notification', loadingLabel: 'Sending...' }
159
+ }
160
+ }
161
+ ```
162
+
163
+ ---
164
+
165
+ ## 2. Adapter Usage
166
+
167
+ Adapters are typed wrappers over `platform.call()`. Import singletons from `@elevasis/sdk/worker`.
168
+
169
+ ### notifications
170
+
171
+ Send a platform notification to the current user. `userId` and `organizationId` are injected server-side -- you never supply them.
172
+
173
+ ```typescript
174
+ import { notifications } from '@elevasis/sdk/worker'
175
+
176
+ await notifications.create({
177
+ category: 'operations', // Any string: 'operations', 'delivery', 'acquisition', etc.
178
+ title: 'Workflow complete',
179
+ message: 'Your email notification was sent successfully.',
180
+ actionUrl: '/operations' // Optional -- link the user can follow
181
+ })
182
+ ```
183
+
184
+ Available methods: `create`.
185
+
186
+ ### llm
187
+
188
+ Generate text or structured output using a language model.
189
+
190
+ ```typescript
191
+ import { llm } from '@elevasis/sdk/worker'
192
+
193
+ const response = await llm.generate({
194
+ provider: 'anthropic', // 'anthropic' | 'openai' | 'openrouter' | 'google'
195
+ model: 'claude-sonnet-5',
196
+ messages: [
197
+ { role: 'user', content: 'Summarize this email body: ' + input.body }
198
+ ]
199
+ })
200
+
201
+ const summary = response.output as string
202
+ ```
203
+
204
+ For structured output, pass a JSON Schema as `responseSchema`:
205
+
206
+ ```typescript
207
+ const response = await llm.generate({
208
+ provider: 'anthropic',
209
+ model: 'claude-sonnet-5',
210
+ messages: [{ role: 'user', content: 'Extract the key action from: ' + input.body }],
211
+ responseSchema: {
212
+ type: 'object',
213
+ properties: { action: { type: 'string' }, urgency: { type: 'string' } }
214
+ }
215
+ })
216
+ const { action, urgency } = response.output as { action: string; urgency: string }
217
+ ```
218
+
219
+ Available methods: `generate`.
220
+
221
+ ### storage
222
+
223
+ Upload and retrieve files scoped to the organization. All paths are automatically prefixed with the organization's storage prefix server-side.
224
+
225
+ ```typescript
226
+ import { storage } from '@elevasis/sdk/worker'
227
+
228
+ // Upload
229
+ await storage.upload({
230
+ bucket: 'operations',
231
+ path: 'email-logs/2026-04-16/batch-01.json',
232
+ content: Buffer.from(JSON.stringify(logData)).toString('base64'),
233
+ contentType: 'application/json'
234
+ })
235
+
236
+ // Download
237
+ const file = await storage.download({
238
+ bucket: 'operations',
239
+ path: 'email-logs/2026-04-16/batch-01.json'
240
+ })
241
+
242
+ // Signed URL for frontend access
243
+ const { signedUrl } = await storage.createSignedUrl({
244
+ bucket: 'operations',
245
+ path: 'email-logs/2026-04-16/batch-01.json',
246
+ expiresIn: 3600
247
+ })
248
+ ```
249
+
250
+ Available methods: `upload`, `download`, `createSignedUrl`, `delete`, `list`.
251
+
252
+ ### scheduler
253
+
254
+ Schedule future or recurring workflow executions.
255
+
256
+ ```typescript
257
+ import { scheduler } from '@elevasis/sdk/worker'
258
+
259
+ const schedule = await scheduler.createSchedule({
260
+ name: 'Daily email digest',
261
+ target: { resourceType: 'workflow', resourceId: 'email-notification' },
262
+ scheduleConfig: {
263
+ type: 'cron',
264
+ expression: '0 9 * * 1-5' // 9am weekdays
265
+ }
266
+ })
267
+
268
+ context.logger.info(`[schedule] Created schedule: ${schedule.id}`)
269
+ ```
270
+
271
+ Available methods: `createSchedule`, `updateAnchor`, `deleteSchedule`, `getSchedule`, `listSchedules`, `cancelSchedule`, `cancelSchedulesByMetadata`, `cancelScheduleByIdempotencyKey`, `findByIdempotencyKey`, `deleteScheduleByIdempotencyKey`.
272
+
273
+ **Note on other adapters:** Integration adapters (`createResendAdapter`, `createAttioAdapter`, etc.) follow a factory pattern -- bind a credential once, use the instance for all calls:
274
+
275
+ ```typescript
276
+ import { createResendAdapter } from '@elevasis/sdk/worker'
277
+
278
+ const resend = createResendAdapter('my-resend-credential')
279
+ await resend.sendEmail({
280
+ to: input.recipientEmail,
281
+ subject: input.subject,
282
+ html: `<p>${input.body}</p>`
283
+ })
284
+ ```
285
+
286
+ See `operations/node_modules/@elevasis/sdk/reference/` for the full adapter reference.
287
+
288
+ ---
289
+
290
+ ## 3. Trigger Patterns from Frontend
291
+
292
+ ### (a) API call via `useApiClient`
293
+
294
+ Use this pattern in React components and hooks. `apiRequest` automatically attaches the auth token and org context.
295
+
296
+ ```typescript
297
+ // ui/src/features/notifications/hooks/useSendEmailNotification.ts
298
+ import { useMutation } from '@tanstack/react-query'
299
+ import { useApiClient } from '@/lib/hooks/useApiClient'
300
+ import type { EmailNotificationInput, EmailNotificationOutput } from '@core/types'
301
+
302
+ export function useSendEmailNotification() {
303
+ const { apiRequest } = useApiClient()
304
+
305
+ return useMutation({
306
+ mutationFn: async (input: EmailNotificationInput) => {
307
+ return apiRequest<EmailNotificationOutput>('/execute', {
308
+ method: 'POST',
309
+ body: JSON.stringify({
310
+ resourceType: 'workflow',
311
+ resourceId: 'email-notification',
312
+ input
313
+ })
314
+ })
315
+ }
316
+ })
317
+ }
318
+ ```
319
+
320
+ Usage in a component:
321
+
322
+ ```tsx
323
+ // ui/src/features/notifications/components/SendNotificationButton.tsx
324
+ import { useSendEmailNotification } from '../hooks/useSendEmailNotification'
325
+
326
+ function SendNotificationButton() {
327
+ const { mutate, isPending, isSuccess } = useSendEmailNotification()
328
+
329
+ return (
330
+ <Button
331
+ loading={isPending}
332
+ onClick={() =>
333
+ mutate({
334
+ recipientEmail: 'user@example.com',
335
+ recipientName: 'Jane Smith',
336
+ subject: 'Your request is ready',
337
+ body: 'Hi Jane, your workflow has completed.',
338
+ category: 'operations'
339
+ })
340
+ }
341
+ >
342
+ Send Notification
343
+ </Button>
344
+ )
345
+ }
346
+ ```
347
+
348
+ For async execution (long-running workflows), use `/execute-async` instead:
349
+
350
+ ```typescript
351
+ return apiRequest<{ executionId: string }>('/execute-async', {
352
+ method: 'POST',
353
+ body: JSON.stringify({
354
+ resourceType: 'workflow',
355
+ resourceId: 'email-notification',
356
+ input
357
+ })
358
+ })
359
+ // Poll /executions/email-notification/:executionId for status
360
+ ```
361
+
362
+ ### (b) Direct SDK dispatch (CLI or scripts)
363
+
364
+ From the project root, use the platform CLI for manual invocations, testing, and scripting:
365
+
366
+ ```bash
367
+ # Describe the schema before executing
368
+ pnpm exec elevasis describe Elevasis/email-notification
369
+
370
+ # Execute synchronously
371
+ pnpm exec elevasis exec Elevasis/email-notification --input '{
372
+ "recipientEmail": "user@example.com",
373
+ "recipientName": "Jane Smith",
374
+ "subject": "Hello",
375
+ "body": "Hi Jane, this is a test notification."
376
+ }'
377
+
378
+ # Execute asynchronously (for long-running workflows)
379
+ pnpm exec elevasis exec Elevasis/email-notification --async --input '{...}'
380
+
381
+ # View a specific execution
382
+ pnpm exec elevasis execution Elevasis/email-notification <executionId>
383
+ ```
384
+
385
+ The `--prod` flag targets `https://api.elevasis.io` and goes **before** the command:
386
+
387
+ ```bash
388
+ pnpm exec elevasis --prod exec Elevasis/email-notification --input '{...}'
389
+ ```
390
+
391
+ ---
392
+
393
+ ## 4. Registry Pattern
394
+
395
+ Workflows are discovered through `operations/src/index.ts`, which exports a `DeploymentSpec` as its default export. The spec assembles deployable runtime resources; it is not a second resource identity catalog.
396
+
397
+ **Pattern:** each feature group in `operations/src/` has its own exports barrel. The top-level spec spreads all groups:
398
+
399
+ ```
400
+ operations/src/
401
+ index.ts # Top-level DeploymentSpec -- never add workflows here directly
402
+ example/
403
+ index.ts # export const workflows = [echo]; export const agents = []
404
+ echo.ts # WorkflowDefinition for 'echo'
405
+ email-notification/
406
+ exports.ts # export const workflows = [emailNotification]; export const agents = []
407
+ index.ts # WorkflowDefinition for 'email-notification'
408
+ ```
409
+
410
+ Top-level registry (`operations/src/index.ts`):
411
+
412
+ ```typescript
413
413
  import type { DeploymentSpec } from '@elevasis/sdk'
414
414
  import { organizationModel } from '@core/config/organization-model'
415
415
  import * as example from './example/index.js'
@@ -420,119 +420,119 @@ const org: DeploymentSpec = {
420
420
  organizationModel,
421
421
  workflows: [...example.workflows, ...emailNotification.workflows],
422
422
  agents: [...example.agents, ...emailNotification.agents]
423
- }
424
- export default org
425
- ```
426
-
427
- Feature group barrel (e.g., `email-notification/exports.ts`):
428
-
429
- ```typescript
430
- import { emailNotification } from './index.js'
431
- import type { WorkflowDefinition } from '@elevasis/sdk'
432
-
433
- export const workflows: WorkflowDefinition[] = [emailNotification]
434
- export const agents: never[] = []
435
- ```
436
-
437
- **Adding a new workflow:**
438
-
439
- 1. Add the resource descriptor to `core/config/organization-model.ts`.
440
- 2. Create `operations/src/<feature>/index.ts` with the `WorkflowDefinition`, deriving `config.resourceId` and `config.type` from the descriptor.
441
- 3. Create `operations/src/<feature>/exports.ts` with `workflows` and `agents` arrays.
442
- 4. Import the group barrel in `operations/src/index.ts` and spread into `workflows`/`agents`.
443
- 5. Run `pnpm -C operations check` to validate descriptor/code alignment, then `pnpm -C operations deploy` to publish.
444
-
445
- **Note:** Use `.js` extensions in imports even though the source is TypeScript. The TypeScript compiler and esbuild bundler both require this for ESM interoperability.
446
-
447
- ---
448
-
449
- ## 5. Testing Custom Workflows And UI
450
-
451
- Package-owned test helpers are the stable way to test custom downstream code. Do not copy template-only tests into a project unless the project owns the matching workflow, route, or component.
452
-
453
- After the bundled package release lands, use these public subpaths:
454
-
455
- ```typescript
456
- import { makeProject } from '@elevasis/core/test-utils'
457
- import { renderWithProviders, mockAuthenticatedUser } from '@elevasis/ui/test-utils'
458
- import { assertResourceRegistry, mockNotifications, runWorkflow } from '@elevasis/sdk/test-utils'
459
- ```
460
-
461
- ### Workflow Smoke Test
462
-
463
- Use `runWorkflow` for project-owned workflows. This tests the workflow contract, step execution, and parsed output without deploying.
464
-
465
- ```typescript
466
- import { describe, expect, it } from 'vitest'
467
- import { runWorkflow, mockNotifications } from '@elevasis/sdk/test-utils'
468
- import { emailNotification } from './index'
469
- import type { EmailNotificationOutput } from '@core/types'
470
-
471
- describe('emailNotification workflow', () => {
472
- it('runs the notify step with a mocked notification adapter', async () => {
473
- const notifications = mockNotifications({
474
- create: { id: 'notification-1' }
475
- })
476
-
477
- const result = await runWorkflow<EmailNotificationOutput>(
478
- emailNotification,
479
- {
480
- recipientEmail: 'jane@example.com',
481
- recipientName: 'Jane',
482
- subject: 'Action ready',
483
- body: 'Your request has been processed.'
484
- },
485
- { adapters: { notification: notifications } }
486
- )
487
-
488
- expect(result.output.delivered).toBe(true)
489
- expect(result.stepEvents.map((event) => event.stepId)).toContain('notify')
490
- })
491
- })
492
- ```
493
-
494
- ### Registry Smoke Test
495
-
496
- Use `assertResourceRegistry` for a project-owned `operations/src/index.ts` manifest. Keep assertions generic unless the project intentionally owns a fixed workflow list.
497
-
498
- ```typescript
499
- import { describe, expect, it } from 'vitest'
500
- import { assertResourceRegistry } from '@elevasis/sdk/test-utils'
501
- import org from './index'
502
-
503
- describe('operations registry', () => {
504
- it('registers a valid operations manifest', () => {
505
- const spec = assertResourceRegistry(org, { organizationName: 'Example Project' })
506
- const workflows = spec.workflows ?? []
507
-
508
- expect(workflows.length + (spec.agents?.length ?? 0)).toBeGreaterThan(0)
509
- expect(new Set(workflows.map((workflow) => workflow.config.resourceId)).size).toBe(workflows.length)
510
- })
511
- })
512
- ```
513
-
514
- ### UI And Core Fixtures
515
-
516
- Use `@elevasis/ui/test-utils` for route/component tests and `@elevasis/core/test-utils` for schema-compatible fixtures.
517
-
518
- ```tsx
519
- import { describe, expect, it } from 'vitest'
520
- import { screen } from '@testing-library/react'
521
- import { makeProject } from '@elevasis/core/test-utils'
522
- import { mockAuthenticatedUser, renderWithProviders } from '@elevasis/ui/test-utils'
523
- import { ProjectCard } from './ProjectCard'
524
-
525
- describe('ProjectCard', () => {
526
- it('renders a project fixture for an authenticated user', () => {
527
- const project = makeProject({ name: 'Website Refresh' })
528
-
529
- renderWithProviders(<ProjectCard project={project} />, {
530
- auth: mockAuthenticatedUser()
531
- })
532
-
533
- expect(screen.getByText('Website Refresh')).toBeInTheDocument()
534
- })
535
- })
536
- ```
537
-
538
- Template tests are examples and smoke coverage. Downstream projects should keep custom tests close to the feature they validate, then consume these package helpers instead of copying scaffold internals.
423
+ }
424
+ export default org
425
+ ```
426
+
427
+ Feature group barrel (e.g., `email-notification/exports.ts`):
428
+
429
+ ```typescript
430
+ import { emailNotification } from './index.js'
431
+ import type { WorkflowDefinition } from '@elevasis/sdk'
432
+
433
+ export const workflows: WorkflowDefinition[] = [emailNotification]
434
+ export const agents: never[] = []
435
+ ```
436
+
437
+ **Adding a new workflow:**
438
+
439
+ 1. Add the resource descriptor to `core/config/organization-model.ts`.
440
+ 2. Create `operations/src/<feature>/index.ts` with the `WorkflowDefinition`, deriving `config.resourceId` and `config.type` from the descriptor.
441
+ 3. Create `operations/src/<feature>/exports.ts` with `workflows` and `agents` arrays.
442
+ 4. Import the group barrel in `operations/src/index.ts` and spread into `workflows`/`agents`.
443
+ 5. Run `pnpm -C operations check` to validate descriptor/code alignment, then `pnpm -C operations deploy` to publish.
444
+
445
+ **Note:** Use `.js` extensions in imports even though the source is TypeScript. The TypeScript compiler and esbuild bundler both require this for ESM interoperability.
446
+
447
+ ---
448
+
449
+ ## 5. Testing Custom Workflows And UI
450
+
451
+ Package-owned test helpers are the stable way to test custom downstream code. Do not copy template-only tests into a project unless the project owns the matching workflow, route, or component.
452
+
453
+ After the bundled package release lands, use these public subpaths:
454
+
455
+ ```typescript
456
+ import { makeProject } from '@elevasis/core/test-utils'
457
+ import { renderWithProviders, mockAuthenticatedUser } from '@elevasis/ui/test-utils'
458
+ import { assertResourceRegistry, mockNotifications, runWorkflow } from '@elevasis/sdk/test-utils'
459
+ ```
460
+
461
+ ### Workflow Smoke Test
462
+
463
+ Use `runWorkflow` for project-owned workflows. This tests the workflow contract, step execution, and parsed output without deploying.
464
+
465
+ ```typescript
466
+ import { describe, expect, it } from 'vitest'
467
+ import { runWorkflow, mockNotifications } from '@elevasis/sdk/test-utils'
468
+ import { emailNotification } from './index'
469
+ import type { EmailNotificationOutput } from '@core/types'
470
+
471
+ describe('emailNotification workflow', () => {
472
+ it('runs the notify step with a mocked notification adapter', async () => {
473
+ const notifications = mockNotifications({
474
+ create: { id: 'notification-1' }
475
+ })
476
+
477
+ const result = await runWorkflow<EmailNotificationOutput>(
478
+ emailNotification,
479
+ {
480
+ recipientEmail: 'jane@example.com',
481
+ recipientName: 'Jane',
482
+ subject: 'Action ready',
483
+ body: 'Your request has been processed.'
484
+ },
485
+ { adapters: { notification: notifications } }
486
+ )
487
+
488
+ expect(result.output.delivered).toBe(true)
489
+ expect(result.stepEvents.map((event) => event.stepId)).toContain('notify')
490
+ })
491
+ })
492
+ ```
493
+
494
+ ### Registry Smoke Test
495
+
496
+ Use `assertResourceRegistry` for a project-owned `operations/src/index.ts` manifest. Keep assertions generic unless the project intentionally owns a fixed workflow list.
497
+
498
+ ```typescript
499
+ import { describe, expect, it } from 'vitest'
500
+ import { assertResourceRegistry } from '@elevasis/sdk/test-utils'
501
+ import org from './index'
502
+
503
+ describe('operations registry', () => {
504
+ it('registers a valid operations manifest', () => {
505
+ const spec = assertResourceRegistry(org, { organizationName: 'Example Project' })
506
+ const workflows = spec.workflows ?? []
507
+
508
+ expect(workflows.length + (spec.agents?.length ?? 0)).toBeGreaterThan(0)
509
+ expect(new Set(workflows.map((workflow) => workflow.config.resourceId)).size).toBe(workflows.length)
510
+ })
511
+ })
512
+ ```
513
+
514
+ ### UI And Core Fixtures
515
+
516
+ Use `@elevasis/ui/test-utils` for route/component tests and `@elevasis/core/test-utils` for schema-compatible fixtures.
517
+
518
+ ```tsx
519
+ import { describe, expect, it } from 'vitest'
520
+ import { screen } from '@testing-library/react'
521
+ import { makeProject } from '@elevasis/core/test-utils'
522
+ import { mockAuthenticatedUser, renderWithProviders } from '@elevasis/ui/test-utils'
523
+ import { ProjectCard } from './ProjectCard'
524
+
525
+ describe('ProjectCard', () => {
526
+ it('renders a project fixture for an authenticated user', () => {
527
+ const project = makeProject({ name: 'Website Refresh' })
528
+
529
+ renderWithProviders(<ProjectCard project={project} />, {
530
+ auth: mockAuthenticatedUser()
531
+ })
532
+
533
+ expect(screen.getByText('Website Refresh')).toBeInTheDocument()
534
+ })
535
+ })
536
+ ```
537
+
538
+ Template tests are examples and smoke coverage. Downstream projects should keep custom tests close to the feature they validate, then consume these package helpers instead of copying scaffold internals.