@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.
- package/dist/cli.cjs +829 -413
- package/dist/index.d.ts +79 -14
- package/dist/index.js +17 -12
- package/dist/templates.js +747 -0
- package/dist/types/templates.d.ts +1 -0
- package/dist/types/worker/index.d.ts +6 -0
- package/dist/types/worker/platform.d.ts +32 -0
- package/dist/worker/index.js +4701 -9
- package/package.json +10 -3
- package/reference/_index.md +95 -0
- package/reference/_navigation.md +104 -0
- package/reference/cli/index.mdx +497 -0
- package/reference/concepts/index.mdx +203 -0
- package/reference/deployment/api.mdx +297 -0
- package/reference/deployment/index.mdx +153 -0
- package/reference/developer/interaction-guidance.mdx +213 -0
- package/reference/framework/agent.mdx +175 -0
- package/reference/framework/documentation.mdx +92 -0
- package/reference/framework/index.mdx +95 -0
- package/reference/framework/memory.mdx +337 -0
- package/reference/framework/project-structure.mdx +294 -0
- package/reference/getting-started/index.mdx +148 -0
- package/reference/index.mdx +113 -0
- package/reference/platform-tools/examples.mdx +187 -0
- package/reference/platform-tools/index.mdx +182 -0
- package/reference/resources/index.mdx +289 -0
- package/reference/resources/patterns.mdx +341 -0
- package/reference/resources/types.mdx +207 -0
- package/reference/roadmap/index.mdx +147 -0
- package/reference/runtime/index.mdx +141 -0
- package/reference/runtime/limits.mdx +77 -0
- package/reference/security/credentials.mdx +141 -0
- package/reference/templates/data-enrichment.mdx +162 -0
- package/reference/templates/email-sender.mdx +135 -0
- package/reference/templates/lead-scorer.mdx +175 -0
- package/reference/templates/pdf-generator.mdx +151 -0
- package/reference/templates/recurring-job.mdx +189 -0
- package/reference/templates/text-classifier.mdx +147 -0
- package/reference/templates/web-scraper.mdx +135 -0
- 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
|