@elevasis/sdk 0.4.5 → 0.4.7

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 (40) hide show
  1. package/dist/cli.cjs +829 -413
  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 +4701 -9
  9. package/package.json +10 -3
  10. package/reference/_index.md +95 -0
  11. package/reference/_navigation.md +104 -0
  12. package/reference/cli/index.mdx +497 -0
  13. package/reference/concepts/index.mdx +203 -0
  14. package/reference/deployment/api.mdx +297 -0
  15. package/reference/deployment/index.mdx +153 -0
  16. package/reference/developer/interaction-guidance.mdx +213 -0
  17. package/reference/framework/agent.mdx +175 -0
  18. package/reference/framework/documentation.mdx +92 -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 +148 -0
  23. package/reference/index.mdx +113 -0
  24. package/reference/platform-tools/examples.mdx +187 -0
  25. package/reference/platform-tools/index.mdx +182 -0
  26. package/reference/resources/index.mdx +289 -0
  27. package/reference/resources/patterns.mdx +341 -0
  28. package/reference/resources/types.mdx +207 -0
  29. package/reference/roadmap/index.mdx +147 -0
  30. package/reference/runtime/index.mdx +141 -0
  31. package/reference/runtime/limits.mdx +77 -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
@@ -0,0 +1,141 @@
1
+ ---
2
+ title: "Credential Security"
3
+ description: "Three-layer credential model: platform tools (server-side injection), http tool (arbitrary APIs), getCredential() (explicit opt-in raw access) -- .env scope and security boundaries"
4
+ loadWhen: "Setting up integrations or configuring tool access"
5
+ ---
6
+
7
+ This reference covers the Elevasis credential security model. Read it when helping a user connect to an external service, set up platform tools, or understand why environment variables are not used in workflows.
8
+
9
+ ---
10
+
11
+ ## The Core Rule
12
+
13
+ Integration credentials are never stored in `.env` and never available via `process.env` inside worker threads.
14
+
15
+ Credentials live in the platform credential system, created via the command center UI. Worker threads access credentials only through controlled channels: `platform.call()` (server-side injection) or `platform.getCredential()` (explicit opt-in).
16
+
17
+ `.env` contains only `ELEVASIS_API_KEY`, used by the CLI for authentication. It is never uploaded to the platform and never injected into workers.
18
+
19
+ ---
20
+
21
+ ## Three-Layer Model
22
+
23
+ ### Layer 1: Platform Tools (Default)
24
+
25
+ All platform tools resolve credentials server-side. The tool dispatcher in the API process looks up the credential by name, injects it into the service call, and returns only the result. Credential values never cross the postMessage boundary into the worker.
26
+
27
+ ```typescript
28
+ // Credential 'my-gmail' is resolved server-side -- value never enters worker memory
29
+ const result = await platform.call({
30
+ tool: 'gmail',
31
+ method: 'sendEmail',
32
+ credential: 'my-gmail',
33
+ params: { to: '...', subject: '...', body: '...' },
34
+ })
35
+ ```
36
+
37
+ This is the default pattern for all integrations that have a platform adapter (Gmail, Attio, Stripe, Resend, etc.).
38
+
39
+ ### Layer 2: HTTP Platform Tool
40
+
41
+ For APIs without a dedicated platform adapter, use the `http` platform tool. Credentials are resolved and injected server-side before the outgoing HTTP request.
42
+
43
+ ```typescript
44
+ const result = await platform.call({
45
+ tool: 'http',
46
+ method: 'request',
47
+ credential: 'my-custom-api',
48
+ params: {
49
+ url: 'https://api.example.com/v1/data',
50
+ method: 'GET',
51
+ headers: { 'Content-Type': 'application/json' },
52
+ },
53
+ })
54
+ // result = { status: 200, headers: {...}, body: {...} }
55
+ ```
56
+
57
+ **Supported credential injection patterns:**
58
+
59
+ | Pattern | Credential Config | How Injected |
60
+ | --- | --- | --- |
61
+ | Bearer token | `{ type: 'bearer', token: 'sk_...' }` | `Authorization: Bearer sk_...` header |
62
+ | API key header | `{ type: 'api-key-header', header: 'X-API-Key', key: 'ak_...' }` | Custom header with key value |
63
+ | Basic auth | `{ type: 'basic', username: '...', password: '...' }` | `Authorization: Basic base64(user:pass)` header |
64
+ | Query parameter | `{ type: 'query-param', param: 'api_key', value: 'ak_...' }` | Appended to URL query string |
65
+ | Body field | `{ type: 'body-field', field: 'apiKey', value: 'ak_...' }` | Merged into JSON request body |
66
+ | Custom header | `{ type: 'custom-header', header: 'X-Custom', value: '...' }` | Arbitrary header with value |
67
+
68
+ The credential type is selected when creating the credential in the command center UI.
69
+
70
+ ### Layer 3: getCredential() (Explicit Opt-In)
71
+
72
+ For cases where a third-party SDK must be initialized with a raw key (e.g., `new Stripe(key)`), use `platform.getCredential()`. This is an explicit opt-in that causes the credential value to enter worker memory.
73
+
74
+ ```typescript
75
+ import { platform } from '@elevasis/sdk/worker'
76
+
77
+ // Explicit opt-in -- secret enters worker memory for duration of execution
78
+ const cred = await platform.getCredential('my-stripe-key')
79
+ const stripe = new Stripe(cred.credentials.secretKey)
80
+ ```
81
+
82
+ `getCredential()` returns `{ provider: string, credentials: Record<string, string> }`. The fields in `credentials` depend on the credential type stored in the command center.
83
+
84
+ **Security guardrails:**
85
+
86
+ - All `getCredential()` calls are logged with credential name, deployment ID, and execution ID
87
+ - Workers are ephemeral (destroyed after each execution) -- no credential persistence
88
+ - Use `platform.call()` with the `http` tool instead when possible
89
+
90
+ ---
91
+
92
+ ## Credential Setup
93
+
94
+ Credentials are created in the command center UI:
95
+
96
+ 1. Open the Elevasis command center
97
+ 2. Navigate to Credentials
98
+ 3. Click "Add Credential"
99
+ 4. Enter a name (e.g., `my-gmail`, `production-attio`, `custom-api`)
100
+ 5. Select the credential type (bearer, api-key-header, basic, etc.)
101
+ 6. Enter the secret values
102
+ 7. Save
103
+
104
+ The credential name is what you pass as `credential: 'my-gmail'` in `platform.call()`.
105
+
106
+ Credential names are case-sensitive. A mismatch causes a `PlatformToolError` with the message "credential not found."
107
+
108
+ ---
109
+
110
+ ## .env Scope
111
+
112
+ `.env` in your workspace contains only:
113
+
114
+ ```
115
+ ELEVASIS_API_KEY=sk_your_key_here
116
+ ```
117
+
118
+ This key authenticates CLI operations (`elevasis deploy`, `elevasis check`, `elevasis exec`). It is never uploaded to the platform and never injected into workers.
119
+
120
+ Do not put integration API keys in `.env`. They will not be available inside workflows.
121
+
122
+ ---
123
+
124
+ ## Choosing a Credential Pattern
125
+
126
+ | Situation | Pattern |
127
+ | --- | --- |
128
+ | Using a supported platform tool (Gmail, Attio, Stripe, etc.) | `platform.call()` with the tool name |
129
+ | Calling an API not in the tool catalog | `platform.call()` with `tool: 'http'` |
130
+ | Third-party SDK that requires a raw key | `platform.getCredential()` |
131
+ | CLI authentication | `ELEVASIS_API_KEY` in `.env` only |
132
+
133
+ ---
134
+
135
+ ## Migrating from elevasis env
136
+
137
+ If you previously set integration credentials via `elevasis env set`, that command no longer exists. Use `elevasis env list` to see what was set, then recreate each entry as a platform credential in the command center. Update workflow code from `process.env.KEY` to `platform.call({ tool: 'http', credential: '...', ... })` or `platform.getCredential('...')`, then redeploy. The `.env` file remains but is scoped to `ELEVASIS_API_KEY` only.
138
+
139
+ ---
140
+
141
+ **Last Updated:** 2026-02-26
@@ -0,0 +1,162 @@
1
+ ---
2
+ title: "Template: Data Enrichment"
3
+ description: "LLM-powered enrichment of existing database records -- read rows, enrich each with an LLM, write results back to Supabase"
4
+ loadWhen: "Applying the data-enrichment workflow template"
5
+ ---
6
+
7
+ **Category:** Data Processing
8
+
9
+ **Platform Tools:** `llm` (text generation), `supabase` (select and update)
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
+ Fetches records from a Supabase table that need enrichment, sends each record to an LLM with a custom prompt, and writes the enriched field back to the table. Supports batching to process multiple records per execution. Suitable for enriching leads with AI-generated summaries, classifying records, extracting structured data from text fields, or scoring records.
21
+
22
+ ---
23
+
24
+ ## Input Schema
25
+
26
+ ```typescript
27
+ z.object({
28
+ table: z.string(), // Supabase table to enrich
29
+ filter: z.record(z.string()).optional(), // Filter rows to enrich (PostgREST format)
30
+ sourceField: z.string(), // Field to send to the LLM as input
31
+ targetField: z.string(), // Field to write enriched output to
32
+ prompt: z.string(), // LLM prompt template (use {value} as placeholder)
33
+ limit: z.number().optional(), // Max rows to process (default: 20)
34
+ })
35
+ ```
36
+
37
+ ## Output Schema
38
+
39
+ ```typescript
40
+ z.object({
41
+ processed: z.number(), // Rows successfully enriched
42
+ skipped: z.number(), // Rows skipped (missing source field, LLM error)
43
+ errors: z.array(z.string()), // Error messages for skipped rows
44
+ })
45
+ ```
46
+
47
+ ---
48
+
49
+ ## Workflow Code Pattern
50
+
51
+ ```typescript
52
+ import type { WorkflowDefinition } from '@elevasis/sdk'
53
+ import { platform } from '@elevasis/sdk/worker'
54
+ import { z } from 'zod'
55
+
56
+ const inputSchema = z.object({
57
+ table: z.string(),
58
+ filter: z.record(z.string()).optional(),
59
+ sourceField: z.string(),
60
+ targetField: z.string(),
61
+ prompt: z.string(),
62
+ limit: z.number().optional(),
63
+ })
64
+ const outputSchema = z.object({
65
+ processed: z.number(),
66
+ skipped: z.number(),
67
+ errors: z.array(z.string()),
68
+ })
69
+
70
+ type Input = z.infer<typeof inputSchema>
71
+
72
+ export const dataEnrichment: WorkflowDefinition = {
73
+ config: {
74
+ resourceId: 'data-enrichment',
75
+ name: 'Data Enrichment',
76
+ type: 'workflow',
77
+ description: 'Enriches database records using an LLM',
78
+ version: '1.0.0',
79
+ status: 'dev',
80
+ },
81
+ contract: { inputSchema, outputSchema },
82
+ steps: {
83
+ enrich: {
84
+ id: 'enrich',
85
+ name: 'Enrich Records',
86
+ description: 'Fetch records, call LLM for each, write results back',
87
+ inputSchema,
88
+ outputSchema,
89
+ handler: async (input, context) => {
90
+ const { table, filter, sourceField, targetField, prompt, limit } = input as Input
91
+
92
+ // Fetch rows to enrich
93
+ const rows = await platform.call({
94
+ tool: 'supabase',
95
+ method: 'select',
96
+ credential: 'my-database',
97
+ params: { table, filter, limit: limit ?? 20 },
98
+ }) as Array<Record<string, unknown>>
99
+
100
+ let processed = 0
101
+ const errors: string[] = []
102
+
103
+ for (const row of rows) {
104
+ const sourceValue = row[sourceField]
105
+ if (!sourceValue || typeof sourceValue !== 'string') {
106
+ errors.push(`Row ${String(row.id)}: missing ${sourceField}`)
107
+ continue
108
+ }
109
+
110
+ try {
111
+ const enrichedPrompt = prompt.replace('{value}', sourceValue)
112
+ const result = await platform.call({
113
+ tool: 'llm',
114
+ method: 'generate',
115
+ params: {
116
+ provider: 'openai',
117
+ model: 'gpt-4o-mini',
118
+ messages: [{ role: 'user', content: enrichedPrompt }],
119
+ },
120
+ }) as string
121
+
122
+ await platform.call({
123
+ tool: 'supabase',
124
+ method: 'update',
125
+ credential: 'my-database',
126
+ params: {
127
+ table,
128
+ filter: { id: `eq.${String(row.id)}` },
129
+ data: { [targetField]: result },
130
+ },
131
+ })
132
+
133
+ context.logger.info('Enriched row', { id: row.id })
134
+ processed++
135
+ } catch (err) {
136
+ const msg = err instanceof Error ? err.message : String(err)
137
+ errors.push(`Row ${String(row.id)}: ${msg}`)
138
+ }
139
+ }
140
+
141
+ return { processed, skipped: errors.length, errors }
142
+ },
143
+ next: null,
144
+ },
145
+ },
146
+ entryPoint: 'enrich',
147
+ }
148
+ ```
149
+
150
+ ---
151
+
152
+ ## Adaptation Notes
153
+
154
+ - **Credential name:** Replace `'my-database'` with the user's database credential name.
155
+ - **LLM provider and model:** Default uses `openai` / `gpt-4o-mini`. Adapt to the user's preference or available providers.
156
+ - **Prompt template:** The `{value}` placeholder is replaced with the source field's value at runtime. Adapt the prompt for the user's specific enrichment task.
157
+ - **Batch size:** Default limit is 20 rows. Increase for bulk processing, decrease for expensive LLM calls.
158
+ - **Skill adaptation:** For beginners, explain what "enrichment" means and give concrete examples (adding a summary field, scoring, categorizing) before generating code.
159
+
160
+ ---
161
+
162
+ **Last Updated:** 2026-02-26
@@ -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