@agentled/cli 0.7.6 → 0.7.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -1
- package/dist/commands/do.js +1 -1
- package/dist/commands/do.js.map +1 -1
- package/dist/commands/examples.d.ts +5 -5
- package/dist/commands/examples.js +8 -2
- package/dist/commands/examples.js.map +1 -1
- package/dist/commands/executions.js +56 -0
- package/dist/commands/executions.js.map +1 -1
- package/dist/commands/onboarding.js +45 -17
- package/dist/commands/onboarding.js.map +1 -1
- package/dist/commands/skills.js +67 -2
- package/dist/commands/skills.js.map +1 -1
- package/dist/commands/workspace.js +41 -0
- package/dist/commands/workspace.js.map +1 -1
- package/dist/utils/agentled-home-content.d.ts +1 -1
- package/dist/utils/agentled-home-content.js +6 -3
- package/dist/utils/agentled-home-content.js.map +1 -1
- package/dist/utils/browser-auth.d.ts +1 -0
- package/dist/utils/browser-auth.js +3 -3
- package/dist/utils/browser-auth.js.map +1 -1
- package/dist/utils/pipeline-lint.js +3 -1
- package/dist/utils/pipeline-lint.js.map +1 -1
- package/dist/utils/skills.d.ts +65 -4
- package/dist/utils/skills.js +147 -5
- package/dist/utils/skills.js.map +1 -1
- package/dist/utils/workspace-folder.d.ts +2 -0
- package/dist/utils/workspace-folder.js +26 -3
- package/dist/utils/workspace-folder.js.map +1 -1
- package/dist/utils/workspace-meta.d.ts +7 -5
- package/dist/utils/workspace-meta.js +30 -7
- package/dist/utils/workspace-meta.js.map +1 -1
- package/llms-full.txt +9 -2
- package/llms.txt +18 -0
- package/package.json +2 -2
- package/patterns/v1/00-design-principles.md +161 -0
- package/patterns/v1/13-entity-pipeline-lifecycle.md +319 -0
- package/scaffolds/README.md +7 -1
- package/scaffolds/child-with-return.json +71 -0
- package/scaffolds/funnel-orchestrator.json +134 -0
- package/scaffolds/kg-process-update.json +88 -0
- package/scaffolds/orchestrator-kg-loop.json +96 -0
- package/scaffolds/source-from-platform.json +78 -0
- package/scaffolds/source-to-kg.json +88 -0
- package/skills/agentled/SKILL.md +66 -737
- package/skills/agentled/references/credits-apps-and-errors.md +40 -0
- package/skills/agentled/references/editing-workflows.md +109 -0
- package/skills/agentled/references/email-workflows.md +122 -0
- package/skills/agentled/references/entity-pipeline.md +106 -0
- package/skills/agentled/references/outreach-and-scale.md +43 -0
- package/skills/agentled/references/persistent-memory.md +106 -0
- package/skills/agentled/references/step-schemas.md +148 -0
- package/skills/agentled/references/workspace-folder.md +74 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# Pattern 13 — Entity Pipeline Lifecycle
|
|
2
|
+
|
|
3
|
+
Build lead, account, candidate, deal, or operator-target systems as a small group of workflows connected by one KG list and explicit status transitions.
|
|
4
|
+
|
|
5
|
+
## When to use
|
|
6
|
+
|
|
7
|
+
Use this pattern when the user describes a recurring business funnel:
|
|
8
|
+
|
|
9
|
+
- source targets from one or more channels
|
|
10
|
+
- save them to a list
|
|
11
|
+
- qualify or score them
|
|
12
|
+
- find the right contact
|
|
13
|
+
- draft or send outreach with approval
|
|
14
|
+
- run the whole process daily or weekly
|
|
15
|
+
|
|
16
|
+
Do not build this as one large workflow unless the user needs a one-off run. The reusable shape is a workflow group.
|
|
17
|
+
|
|
18
|
+
## Canonical shape
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
Source Channel A -> kg.upsert-rows(status: "new")
|
|
22
|
+
Source Channel B -> kg.upsert-rows(status: "new")
|
|
23
|
+
Score / Qualify -> read status "new" -> status "qualified" | "needs_review" | "disqualified"
|
|
24
|
+
Find Contact -> read status "qualified" -> status "contact_found" | "contact_missing"
|
|
25
|
+
Draft Outreach -> read status "contact_found" -> status "ready_to_contact"
|
|
26
|
+
Send / Queue Outreach -> read status "ready_to_contact" -> status "contacted"
|
|
27
|
+
Daily / Weekly Orchestrator -> runs the stages in order, computes KPIs, writes a digest
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The status names are examples. Use domain language that matches the list, then document it.
|
|
31
|
+
|
|
32
|
+
## Build order
|
|
33
|
+
|
|
34
|
+
1. Create the KG list first.
|
|
35
|
+
2. Build one sourcing workflow that writes rows with `status: "new"` and stable `userKey`.
|
|
36
|
+
3. Build one scoring workflow that reads `status: "new"` and writes a decisive next status.
|
|
37
|
+
4. Build contact discovery only after scoring produces enough qualified rows.
|
|
38
|
+
5. Build outreach as a separate workflow with approval. Never send directly from sourcing or scoring.
|
|
39
|
+
6. Build the scheduled orchestrator last. It should coordinate already-working workflows, not hide broken stages.
|
|
40
|
+
|
|
41
|
+
## Recommended CLI scaffolds
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Source rows from a native app into a KG list.
|
|
45
|
+
agentled workflows scaffold source-from-platform --out 01-source.json
|
|
46
|
+
|
|
47
|
+
# Score / qualify rows against an ICP and store the result.
|
|
48
|
+
agentled workflows scaffold lead-scoring-kg --out 02-score-qualify.json
|
|
49
|
+
|
|
50
|
+
# Draft/send approval-gated outreach from matched list rows.
|
|
51
|
+
agentled workflows scaffold list-match-email --out 04-outreach.json
|
|
52
|
+
|
|
53
|
+
# Coordinate the recurring funnel once child workflows are working.
|
|
54
|
+
agentled workflows scaffold funnel-orchestrator --out 99-orchestrator.json
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## KG list contract
|
|
58
|
+
|
|
59
|
+
Every entity list used by this pattern needs:
|
|
60
|
+
|
|
61
|
+
| Field | Purpose |
|
|
62
|
+
|---|---|
|
|
63
|
+
| `userKey` | Stable dedupe key. Prefer domain, email, LinkedIn URL, or canonical source URL. |
|
|
64
|
+
| `status` | Indexed workflow phase. Every stage reads one status and writes the next. |
|
|
65
|
+
| `source` / `source_channel` | Where the row came from. Useful for quality and cost analysis. |
|
|
66
|
+
| `sourced_at` | When the row first entered the funnel. |
|
|
67
|
+
| score fields | `fitScore`, `fitReasoning`, `qualificationReason`, or domain-specific equivalents. |
|
|
68
|
+
| contact fields | `buyerRole`, `contactName`, `contactEmail`, `contactLinkedin`, `contactConfidence`. |
|
|
69
|
+
| outreach fields | `outreachAngle`, `firstCallHook`, `notes`, `lastContactedAt`, `nextActionAt`. |
|
|
70
|
+
| delivery fields | `messageId`, `threadId`, `channelUsed`, `sentAt`, `deliveryStatus`, `bouncedAt`, `bounceReason`. |
|
|
71
|
+
| engagement fields | `openedAt`, `openCount`, `clickedAt`, `clickCount`, `lastClickedUrl`, `repliedAt`, `replySentiment`. |
|
|
72
|
+
|
|
73
|
+
Use `mergeStrategy: "merge"` whenever a source workflow upserts rows. Re-sourcing the same entity must not wipe scoring or outreach fields.
|
|
74
|
+
|
|
75
|
+
## Status rules
|
|
76
|
+
|
|
77
|
+
1. Each workflow reads exactly one input status.
|
|
78
|
+
2. Each workflow writes a forward status, never back to an earlier phase.
|
|
79
|
+
3. Write status before customer-visible side effects when possible. This prevents duplicate sends on retries.
|
|
80
|
+
4. Use a review status for uncertain rows instead of forcing them into qualified or disqualified.
|
|
81
|
+
5. Keep disqualified rows. They are calibration data for future sourcing and scoring.
|
|
82
|
+
|
|
83
|
+
## Sourcing workflow
|
|
84
|
+
|
|
85
|
+
Purpose: create raw rows only.
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
trigger -> source/search app -> normalize -> kg.upsert-rows(status: "new") -> done
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Rules:
|
|
92
|
+
|
|
93
|
+
- Do not score in the source workflow.
|
|
94
|
+
- Do not find contacts in the source workflow unless the source returns them cheaply.
|
|
95
|
+
- Do not draft outreach in the source workflow.
|
|
96
|
+
- Do write `source_channel`, `source_url`, and enough evidence for the scorer to judge quality.
|
|
97
|
+
|
|
98
|
+
## Score / qualify workflow
|
|
99
|
+
|
|
100
|
+
Purpose: be strict and reduce noise.
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
trigger -> kg.read-list(status: "new") -> read ICP/rubric -> score rows -> kg.upsert-rows(status buckets) -> done
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Use three buckets:
|
|
107
|
+
|
|
108
|
+
- `qualified`: ready for contact discovery or outreach drafting
|
|
109
|
+
- `needs_review`: plausible, but a human should inspect before outreach
|
|
110
|
+
- `disqualified`: do not contact
|
|
111
|
+
|
|
112
|
+
If the list schema does not allow `needs_review`, use the workspace's closest status such as `enriched` or `review`.
|
|
113
|
+
|
|
114
|
+
## Contact discovery workflow
|
|
115
|
+
|
|
116
|
+
Purpose: find one realistic buyer path for qualified rows.
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
trigger -> kg.read-list(status: "qualified") -> find person/email/LinkedIn -> verify -> kg.upsert-rows(status: "contact_found" or "contact_missing")
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Rules:
|
|
123
|
+
|
|
124
|
+
- Use the best available source for the channel: LinkedIn/profile enrichment for the person path, email-finder/verification actions for email, and company-domain fallback only when no person-level contact is available.
|
|
125
|
+
- Store both person and channel fields when found: `contactName`, `buyerRole`, `contactLinkedin`, `contactEmail`, `contactSource`, `contactConfidence`.
|
|
126
|
+
- Prefer a likely role over invented person details.
|
|
127
|
+
- Keep confidence fields.
|
|
128
|
+
- Put missing-contact rows in their own status so they can be retried later with a different provider.
|
|
129
|
+
|
|
130
|
+
## Outreach workflow
|
|
131
|
+
|
|
132
|
+
Purpose: prepare or send customer-visible messages with approval.
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
trigger -> kg.read-list(status: "contact_found") -> draft message -> approval -> schedule-email -> kg.upsert-rows(status: "contacted")
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
For email, use the composed email pattern:
|
|
139
|
+
|
|
140
|
+
- `pipelineStepPrompt.type: "email"`
|
|
141
|
+
- `renderer: { type: "Email", config: { fromContextKey: "outreachProfile" } }`
|
|
142
|
+
- `onApproval.action: "schedule-email"`
|
|
143
|
+
- `next.conditions.approvalRequired: true`
|
|
144
|
+
|
|
145
|
+
If the user only wants drafts, write the draft into `notes` and move rows to `ready_to_contact`. Do not send.
|
|
146
|
+
|
|
147
|
+
Mark a row `contacted` only after the send/queue action succeeds, not when the draft is generated. Store the send result fields (`messageId`, `threadId`, `channelUsed`, `sentAt`, `lastContactedAt`) at the same time so future follow-up workflows can dedupe and thread replies.
|
|
148
|
+
|
|
149
|
+
### Approval step examples
|
|
150
|
+
|
|
151
|
+
**Email send with approval**
|
|
152
|
+
|
|
153
|
+
Use the first-class composed email shape. This is the only supported pattern for approval-gated email sending; do not model this as `draft-email -> gmail.send-email`.
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
{
|
|
157
|
+
"id": "send-outreach-email",
|
|
158
|
+
"type": "aiAction",
|
|
159
|
+
"name": "Send Outreach Email",
|
|
160
|
+
"pipelineStepPrompt": {
|
|
161
|
+
"type": "email",
|
|
162
|
+
"template": "Draft a concise outreach email for this target.\\n\\nTarget row:\\n{{steps.read-target.rows.0}}\\n\\nUse the outreach angle and first-call hook already stored on the row. Return email-safe HTML only.",
|
|
163
|
+
"responseStructure": {
|
|
164
|
+
"email": {
|
|
165
|
+
"from": "{{context.outreachProfile.fromEmail}}",
|
|
166
|
+
"to": "{{steps.read-target.rows.0.contactEmail}}",
|
|
167
|
+
"subject": "",
|
|
168
|
+
"body": "",
|
|
169
|
+
"bodyType": "html"
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
"responseType": "json"
|
|
173
|
+
},
|
|
174
|
+
"renderer": {
|
|
175
|
+
"type": "Email",
|
|
176
|
+
"config": { "fromContextKey": "outreachProfile" }
|
|
177
|
+
},
|
|
178
|
+
"integrations": [
|
|
179
|
+
{
|
|
180
|
+
"type": "oneOf",
|
|
181
|
+
"label": "Email",
|
|
182
|
+
"connectorType": "email",
|
|
183
|
+
"options": [
|
|
184
|
+
{ "name": "Gmail", "url": "https://gmail.com", "isUserAccountConnectionRequired": true },
|
|
185
|
+
{ "name": "Outlook", "url": "https://outlook.com", "isUserAccountConnectionRequired": true }
|
|
186
|
+
],
|
|
187
|
+
"selectionHint": "preferConnected"
|
|
188
|
+
}
|
|
189
|
+
],
|
|
190
|
+
"onApproval": {
|
|
191
|
+
"executedText": "Email sent by {{name}} at {{date}}",
|
|
192
|
+
"scheduledText": "Email scheduled for {{date}} by {{name}}",
|
|
193
|
+
"failedText": "Email failed to send.",
|
|
194
|
+
"action": "schedule-email"
|
|
195
|
+
},
|
|
196
|
+
"creditCost": 5,
|
|
197
|
+
"next": {
|
|
198
|
+
"stepId": "mark-contacted",
|
|
199
|
+
"conditions": { "approvalRequired": true }
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`mark-contacted` should run after the email step completes and upsert the row with `status: "contacted"`, `channelUsed: "email"`, `sentAt`, `lastContactedAt`, and any returned `messageId` / `threadId`.
|
|
205
|
+
|
|
206
|
+
**LinkedIn connection note with approval**
|
|
207
|
+
|
|
208
|
+
Use the `LinkedInMessage` renderer for the approval UI. A LinkedIn connection note is copy-only unless the workspace has a real LinkedIn outreach integration connected and verified. Do not use `schedule-linkedin-content`; that action is for publishing posts, not sending connection notes.
|
|
209
|
+
|
|
210
|
+
```json
|
|
211
|
+
{
|
|
212
|
+
"id": "draft-linkedin-connection-note",
|
|
213
|
+
"type": "aiAction",
|
|
214
|
+
"name": "Draft LinkedIn Connection Note",
|
|
215
|
+
"description": "Draft a LinkedIn connection note for review.",
|
|
216
|
+
"pipelineStepPrompt": {
|
|
217
|
+
"type": "analysis",
|
|
218
|
+
"template": "Draft a LinkedIn connection note for this target.\\n\\nTarget row:\\n{{steps.read-target.rows.0}}\\n\\nRules:\\n- connectionNote.body must be 300 characters or less\\n- no links\\n- plain text only\\n- mention the most concrete manual operation angle from outreachAngle or firstCallHook\\n\\nReturn JSON only.",
|
|
219
|
+
"responseStructure": {
|
|
220
|
+
"outreachMessage": {
|
|
221
|
+
"connectionNote": {
|
|
222
|
+
"mode": "connection_note",
|
|
223
|
+
"body": "≤300 chars, no links"
|
|
224
|
+
},
|
|
225
|
+
"linkedinUrl": "{{steps.read-target.rows.0.contactLinkedin}}",
|
|
226
|
+
"recipientName": "{{steps.read-target.rows.0.contactName}}"
|
|
227
|
+
},
|
|
228
|
+
"reasoning": "Why this note is relevant"
|
|
229
|
+
},
|
|
230
|
+
"responseType": "json"
|
|
231
|
+
},
|
|
232
|
+
"renderer": {
|
|
233
|
+
"type": "LinkedInMessage",
|
|
234
|
+
"config": { "responseField": "outreachMessage" }
|
|
235
|
+
},
|
|
236
|
+
"onApproval": {
|
|
237
|
+
"executedText": "LinkedIn connection note approved by {{name}} at {{date}}",
|
|
238
|
+
"scheduledText": "Waiting for approval to proceed",
|
|
239
|
+
"failedText": "Failed to proceed to next step",
|
|
240
|
+
"action": "PROCEED"
|
|
241
|
+
},
|
|
242
|
+
"creditCost": 3,
|
|
243
|
+
"next": {
|
|
244
|
+
"stepId": "queue-linkedin-note",
|
|
245
|
+
"conditions": { "approvalRequired": true }
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Then add a downstream step that either calls the verified LinkedIn/PhantomBuster connection-note action or queues the payload for manual send:
|
|
251
|
+
|
|
252
|
+
```json
|
|
253
|
+
{
|
|
254
|
+
"id": "queue-linkedin-note",
|
|
255
|
+
"type": "code",
|
|
256
|
+
"name": "Queue LinkedIn Connection Note",
|
|
257
|
+
"description": "Return the approved LinkedIn note payload for manual send or for a connected LinkedIn outreach action.",
|
|
258
|
+
"codeConfig": {
|
|
259
|
+
"language": "javascript",
|
|
260
|
+
"code": "const message = {{steps.draft-linkedin-connection-note.outreachMessage}};\\nif (!message?.linkedinUrl) return { queued: false, reason: 'missing_linkedin_url' };\\nif (!message?.connectionNote?.body || message.connectionNote.body.length > 300) return { queued: false, reason: 'invalid_connection_note' };\\nreturn { queued: true, channelUsed: 'linkedin_connection_note', linkedinUrl: message.linkedinUrl, note: message.connectionNote.body };"
|
|
261
|
+
},
|
|
262
|
+
"next": { "stepId": "mark-linkedin-contacted" }
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Only mark the row `contacted` after the real send/queue step succeeds. For manual LinkedIn sends, use a status such as `linkedin_note_queued` or `ready_for_manual_linkedin` until a user confirms the note was actually sent.
|
|
267
|
+
|
|
268
|
+
## Engagement tracking workflow
|
|
269
|
+
|
|
270
|
+
Purpose: update rows after outreach based on provider events or inbox polling.
|
|
271
|
+
|
|
272
|
+
```
|
|
273
|
+
email/linkedin event or polling -> match message/thread/contact -> kg.upsert-rows(status or tracking fields) -> optional follow-up workflow
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Track these events when the channel supports them:
|
|
277
|
+
|
|
278
|
+
- `delivered`: set `deliveryStatus: "delivered"` and `deliveredAt`.
|
|
279
|
+
- `bounced`: set `deliveryStatus: "bounced"`, `bouncedAt`, `bounceReason`, and usually stop follow-ups.
|
|
280
|
+
- `opened`: set `openedAt` on first open and increment `openCount`.
|
|
281
|
+
- `clicked`: set `clickedAt`, increment `clickCount`, and store `lastClickedUrl`.
|
|
282
|
+
- `replied`: set `repliedAt`, classify `replySentiment`, and move to a terminal or review status such as `replied_interested`, `replied_declined`, or `reply_needs_review`.
|
|
283
|
+
|
|
284
|
+
Agentled workflow email supports open/click tracking for HTML emails when tracking is enabled: the email service injects an open pixel and rewrites links through tracking redirects, then writes `TrackingEvent` rows with `eventType: "opened"` or `eventType: "clicked"`.
|
|
285
|
+
|
|
286
|
+
Important: opens and clicks are still not guaranteed signals. Some recipients block images, prefetch links, or use privacy proxies. Treat them as engagement hints, not proof that a human read the message. Delivery and bounce tracking are provider-event concerns and should be handled separately from open/click tracking.
|
|
287
|
+
|
|
288
|
+
## Orchestrator workflow
|
|
289
|
+
|
|
290
|
+
Purpose: run the machine on a cadence and report health.
|
|
291
|
+
|
|
292
|
+
```
|
|
293
|
+
schedule -> read new count -> call scoring workflow -> wait for loop completion
|
|
294
|
+
-> read qualified count -> call contact/outreach workflow
|
|
295
|
+
-> compute KPIs -> share/send digest -> done
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Build the orchestrator after the child workflows validate and have passed at least one manual run. It should use `agentled.call-workflow` or `agentled.start-workflow` and loop over rows or stages with `loop_completion` gates.
|
|
299
|
+
|
|
300
|
+
## Outreach metrics
|
|
301
|
+
|
|
302
|
+
For outbound or email-heavy entity pipelines, configure business metrics in `analyticsConfig`:
|
|
303
|
+
|
|
304
|
+
- `prospects_contacted`: count or extract rows moved to a contacted/sent status.
|
|
305
|
+
- `positive_replies`: count or extract rows moved to `replied_positive`, `replied_interested`, `meeting_scheduled`, `paid`, or the workspace's equivalent positive outcome.
|
|
306
|
+
- `pcpl`: prospects contacted per positive lead, computed as `prospects_contacted / positive_replies` with `ratioMode: "raw"`.
|
|
307
|
+
- `positive_reply_rate`: optional percentage-style ratio, `positive_replies / prospects_contacted`.
|
|
308
|
+
- `meetings_booked` / `paid_confirmations`: downstream conversion, if the workspace tracks it.
|
|
309
|
+
- `bounce_count` / `failed_send_count`: deliverability guardrail, if available.
|
|
310
|
+
|
|
311
|
+
Literal PCPL should use `type: "ratio"` with `ratioMode: "raw"`. A plain ratio metric without `ratioMode` keeps the legacy percentage behavior for rates.
|
|
312
|
+
|
|
313
|
+
## What to tell the user
|
|
314
|
+
|
|
315
|
+
Explain this pattern in business terms:
|
|
316
|
+
|
|
317
|
+
> "We will create a simple operating funnel: one workflow finds candidates, one decides who is worth contacting, one finds the right contact, one prepares outreach for approval, and one scheduled workflow keeps the whole process moving."
|
|
318
|
+
|
|
319
|
+
Avoid describing this as an AI stack, orchestration graph, or automation framework unless the user asks for implementation details.
|
package/scaffolds/README.md
CHANGED
|
@@ -20,16 +20,22 @@ Every bundled scaffold:
|
|
|
20
20
|
4. Uses domain-agnostic placeholder names (`candidate`, `metric_a`,
|
|
21
21
|
`entity_id`) — not specific verticals.
|
|
22
22
|
|
|
23
|
+
> **Read [`patterns/v1/00-design-principles.md`](../patterns/v1/00-design-principles.md) first.** It defines the four pillars (idempotency, small scope, KG-list-as-connector, design-as-data) and the decision tree that tells you which scaffold to pick.
|
|
24
|
+
|
|
23
25
|
## Catalog
|
|
24
26
|
|
|
25
27
|
| Slug | Pattern(s) | Shape |
|
|
26
28
|
|------|-----------|-------|
|
|
27
29
|
| `minimal` | — | trigger → milestone (smallest valid pipeline) |
|
|
30
|
+
| `ai-with-tools` | — | trigger → `aiActionWithTools` (web_search + workspace_memory) → milestone |
|
|
28
31
|
| `email-polling-dedup` | 02 + 13 | schedule → fetch emails (label dedup) → loop process → add label |
|
|
29
32
|
| `lead-scoring-kg` | 04 + 09 | trigger → kg.read-list → AI scoring loop → knowledgeSync → report |
|
|
30
33
|
| `list-match-email` | 08 | trigger → kg.read-list → AI match top candidates → composed email (approval gate) → knowledgeSync |
|
|
31
34
|
| `extract-threshold-alert` | 06 + 09 | trigger → AI extract → threshold check (code) → external update → conditional Slack alert → knowledgeSync |
|
|
32
|
-
| `
|
|
35
|
+
| `source-to-kg` | 12 + 15a | schedule → generate queries → search → extract → kg.upsert-rows(`userKey`, status:`new`) — idempotent sourcing half of a two-workflow group |
|
|
36
|
+
| `kg-process-update` | 12 + 15b | schedule → kg.read-list(status:`new`) → loop process → kg.update-rows(status:`processed`) — processing half of a two-workflow group |
|
|
37
|
+
| `child-with-return` | 05 + 12 | manual → enrich → score → `return` (internal:true, called via call-workflow) |
|
|
38
|
+
| `orchestrator-kg-loop` | 11 | manual → kg.read-list → loop call-workflow per row → wait loop_completion → kg.update-rows → summarize |
|
|
33
39
|
|
|
34
40
|
## Bring your own scaffolds
|
|
35
41
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Child Workflow with Return",
|
|
3
|
+
"goal": "A reusable child workflow that takes input via call-workflow and returns a structured result. Terminal step is `return`, not `milestone`.",
|
|
4
|
+
"description": "Pattern 12 (child-workflow-contracts). Mark `context.executionInputConfig.internal: true` so the UI hides the Run button — this workflow is invoked only by orchestrators via `call-workflow`. The return contract is explicit: every field a caller can read is declared in `returnConfig.fields`.",
|
|
5
|
+
"status": "draft",
|
|
6
|
+
"context": {
|
|
7
|
+
"executionInputConfig": {
|
|
8
|
+
"title": "Score Entity (called via orchestrator)",
|
|
9
|
+
"description": "This is a child workflow. Run it from an orchestrator via call-workflow, not by hand.",
|
|
10
|
+
"runCTALabel": "Score",
|
|
11
|
+
"allowRegeneration": false,
|
|
12
|
+
"internal": true,
|
|
13
|
+
"fields": [
|
|
14
|
+
{ "name": "entity_id", "label": "Entity ID", "type": "text", "required": true },
|
|
15
|
+
{ "name": "entity_url", "label": "Entity URL", "type": "text", "required": true }
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"steps": [
|
|
20
|
+
{
|
|
21
|
+
"id": "start",
|
|
22
|
+
"type": "trigger",
|
|
23
|
+
"name": "Called by Orchestrator",
|
|
24
|
+
"pipelineStepStartConditions": { "trigger": { "type": "manual" } },
|
|
25
|
+
"next": { "stepId": "enrich" }
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": "enrich",
|
|
29
|
+
"type": "aiAction",
|
|
30
|
+
"name": "Enrich Entity",
|
|
31
|
+
"pipelineStepPrompt": {
|
|
32
|
+
"template": "Replace with your real enrichment logic.\n\nEntity: {{input.entity_id}}\nURL: {{input.entity_url}}\n\nReturn the enriched fields the score step will consume.",
|
|
33
|
+
"responseStructure": {
|
|
34
|
+
"enriched": "object — fields the score step needs"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"creditCost": 6,
|
|
38
|
+
"next": { "stepId": "score" }
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "score",
|
|
42
|
+
"type": "aiAction",
|
|
43
|
+
"name": "Score Entity",
|
|
44
|
+
"pipelineStepPrompt": {
|
|
45
|
+
"template": "Score this entity against the rubric stored in KG text (`kg.read-text` with key `design.scoring-rubric` — load upstream if you need it). Return total_score (0-100), decision (qualify|nurture|reject), and reasoning.\n\nEntity: {{input.entity_id}}\nEnriched: {{steps.enrich.enriched}}",
|
|
46
|
+
"responseStructure": {
|
|
47
|
+
"total_score": "number (0-100)",
|
|
48
|
+
"decision": "string (qualify | nurture | reject)",
|
|
49
|
+
"reasoning": "string"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"creditCost": 8,
|
|
53
|
+
"next": { "stepId": "return-result" }
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"id": "return-result",
|
|
57
|
+
"type": "return",
|
|
58
|
+
"name": "Return Score to Caller",
|
|
59
|
+
"returnConfig": {
|
|
60
|
+
"source": "executionContent",
|
|
61
|
+
"fields": [
|
|
62
|
+
{ "name": "entity_id", "stepId": "start", "field": "entity_id" },
|
|
63
|
+
{ "name": "total_score", "stepId": "score", "field": "total_score" },
|
|
64
|
+
{ "name": "decision", "stepId": "score", "field": "decision" },
|
|
65
|
+
{ "name": "reasoning", "stepId": "score", "field": "reasoning" },
|
|
66
|
+
{ "name": "enriched", "stepId": "enrich", "field": "enriched" }
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_scaffold": "funnel-orchestrator",
|
|
3
|
+
"_description": "Scheduled KG funnel coordinator: read new rows, call child scoring workflow, read qualified rows, queue outreach workflow, compute KPIs, and finish with a digest.",
|
|
4
|
+
"name": "Scheduled Funnel Orchestrator",
|
|
5
|
+
"goal": "Run a recurring funnel pass across already-working child workflows and report what moved.",
|
|
6
|
+
"description": "Use after sourcing, scoring, contact discovery, and outreach workflows have each passed manual validation.",
|
|
7
|
+
"status": "draft",
|
|
8
|
+
"context": {
|
|
9
|
+
"executionInputConfig": {
|
|
10
|
+
"title": "Run Funnel",
|
|
11
|
+
"description": "Manual override for the scheduled orchestrator.",
|
|
12
|
+
"runCTALabel": "Run funnel",
|
|
13
|
+
"fields": [
|
|
14
|
+
{ "name": "source_list_key", "label": "Source list key", "type": "text", "required": true, "defaultValue": "leads", "description": "Knowledge list key (e.g. \"leads\", \"startups\")." },
|
|
15
|
+
{ "name": "new_status", "label": "New status", "type": "text", "required": true, "defaultValue": "new" },
|
|
16
|
+
{ "name": "qualified_status", "label": "Qualified status", "type": "text", "required": true, "defaultValue": "qualified" },
|
|
17
|
+
{ "name": "score_workflow_id", "label": "Scoring workflow ID", "type": "text", "required": true, "description": "Workflow ID for the child scoring workflow." },
|
|
18
|
+
{ "name": "outreach_workflow_id", "label": "Outreach workflow ID", "type": "text", "required": true, "description": "Workflow ID for the child outreach workflow." }
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"steps": [
|
|
23
|
+
{
|
|
24
|
+
"id": "start",
|
|
25
|
+
"type": "trigger",
|
|
26
|
+
"name": "Scheduled funnel run",
|
|
27
|
+
"pipelineStepStartConditions": {
|
|
28
|
+
"trigger": {
|
|
29
|
+
"type": "schedule",
|
|
30
|
+
"description": "Run weekly.",
|
|
31
|
+
"config": { "frequency": "weekly", "dayOfWeek": "monday", "time": "09:00" }
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"next": { "stepId": "read-new" }
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"id": "read-new",
|
|
38
|
+
"type": "appAction",
|
|
39
|
+
"name": "Read new rows",
|
|
40
|
+
"app": { "id": "kg", "actionId": "kg.read-list", "source": "native" },
|
|
41
|
+
"stepInputData": {
|
|
42
|
+
"listKey": "{{input.source_list_key}}",
|
|
43
|
+
"status": "{{input.new_status || 'new'}}",
|
|
44
|
+
"limit": "25"
|
|
45
|
+
},
|
|
46
|
+
"next": { "stepId": "score-rows" }
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"id": "score-rows",
|
|
50
|
+
"type": "appAction",
|
|
51
|
+
"name": "Run scoring workflow",
|
|
52
|
+
"app": { "id": "agentled", "actionId": "agentled.call-workflow", "source": "native" },
|
|
53
|
+
"stepInputData": {
|
|
54
|
+
"workflowId": "{{input.score_workflow_id}}",
|
|
55
|
+
"executionInputData": "{\"row\": {{newRow}}, \"listKey\": \"{{input.source_list_key}}\"}",
|
|
56
|
+
"timeoutMinutes": "20"
|
|
57
|
+
},
|
|
58
|
+
"loopConfig": {
|
|
59
|
+
"source": "executionContent",
|
|
60
|
+
"config": { "stepId": "read-new", "field": "listEntries" },
|
|
61
|
+
"ItemAlias": "newRow",
|
|
62
|
+
"loopCompletion": { "criteria": "all_items_processed" }
|
|
63
|
+
},
|
|
64
|
+
"creditCost": 0,
|
|
65
|
+
"next": { "stepId": "read-qualified" }
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"id": "read-qualified",
|
|
69
|
+
"type": "appAction",
|
|
70
|
+
"name": "Read qualified rows",
|
|
71
|
+
"app": { "id": "kg", "actionId": "kg.read-list", "source": "native" },
|
|
72
|
+
"stepInputData": {
|
|
73
|
+
"listKey": "{{input.source_list_key}}",
|
|
74
|
+
"status": "{{input.qualified_status || 'qualified'}}",
|
|
75
|
+
"limit": "25"
|
|
76
|
+
},
|
|
77
|
+
"entryConditions": {
|
|
78
|
+
"onCriteriaFail": "wait",
|
|
79
|
+
"conditionText": "Wait for scoring child workflows to complete.",
|
|
80
|
+
"criteria": [
|
|
81
|
+
{ "type": "loop_completion", "stepId": "score-rows", "operator": "==", "value": true }
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
"next": { "stepId": "queue-outreach" }
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"id": "queue-outreach",
|
|
88
|
+
"type": "appAction",
|
|
89
|
+
"name": "Queue outreach workflow",
|
|
90
|
+
"app": { "id": "agentled", "actionId": "agentled.start-workflow", "source": "native" },
|
|
91
|
+
"stepInputData": {
|
|
92
|
+
"workflowId": "{{input.outreach_workflow_id}}",
|
|
93
|
+
"executionInputData": "{\"row\": {{qualifiedRow}}, \"listKey\": \"{{input.source_list_key}}\"}"
|
|
94
|
+
},
|
|
95
|
+
"entryConditions": {
|
|
96
|
+
"onCriteriaFail": "skip",
|
|
97
|
+
"conditionText": "Skip outreach if no qualified rows are available.",
|
|
98
|
+
"criteria": [
|
|
99
|
+
{ "variable": "{{steps.read-qualified.count}}", "operator": ">", "value": 0 }
|
|
100
|
+
]
|
|
101
|
+
},
|
|
102
|
+
"loopConfig": {
|
|
103
|
+
"source": "executionContent",
|
|
104
|
+
"config": { "stepId": "read-qualified", "field": "listEntries" },
|
|
105
|
+
"ItemAlias": "qualifiedRow",
|
|
106
|
+
"loopCompletion": { "criteria": "all_items_processed" }
|
|
107
|
+
},
|
|
108
|
+
"creditCost": 0,
|
|
109
|
+
"next": { "stepId": "summarize-run" }
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"id": "summarize-run",
|
|
113
|
+
"type": "aiAction",
|
|
114
|
+
"name": "Summarize run",
|
|
115
|
+
"pipelineStepPrompt": {
|
|
116
|
+
"template": "Create a concise internal funnel digest.\n\nNew rows read: {{steps.read-new.count}}\nQualified rows read: {{steps.read-qualified.count}}\nScoring results: {{steps.score-rows}}\nOutreach starts: {{steps.queue-outreach}}\n\nReturn JSON only.",
|
|
117
|
+
"responseStructure": {
|
|
118
|
+
"summary": "string",
|
|
119
|
+
"newCount": "number",
|
|
120
|
+
"qualifiedCount": "number",
|
|
121
|
+
"outreachQueuedCount": "number",
|
|
122
|
+
"recommendations": ["string"]
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
"creditCost": 3,
|
|
126
|
+
"next": { "stepId": "done" }
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"id": "done",
|
|
130
|
+
"type": "milestone",
|
|
131
|
+
"name": "Funnel run complete"
|
|
132
|
+
}
|
|
133
|
+
]
|
|
134
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "KG Process + Update (status state machine)",
|
|
3
|
+
"goal": "Read entities from a KG list filtered by status, process each in a loop, and flip status when done so the same row is never re-processed.",
|
|
4
|
+
"description": "Pattern 15b (processing half of the Source -> Process split) and pattern 12 (KG status as the API between workflows). Pair with `source-to-kg` as the producer. Loop processes one row at a time; after loop completion, `kg.update-rows` flips status to `processed` so the next run only sees what's still `new`.",
|
|
5
|
+
"status": "draft",
|
|
6
|
+
"context": {
|
|
7
|
+
"executionInputConfig": {
|
|
8
|
+
"title": "Process New Entities",
|
|
9
|
+
"description": "Process entities flagged as `new` in the shared KG list.",
|
|
10
|
+
"runCTALabel": "Process",
|
|
11
|
+
"allowRegeneration": false,
|
|
12
|
+
"fields": [
|
|
13
|
+
{ "name": "list_key", "label": "KG list key", "type": "text", "required": true }
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"steps": [
|
|
18
|
+
{
|
|
19
|
+
"id": "schedule",
|
|
20
|
+
"type": "trigger",
|
|
21
|
+
"name": "Daily process",
|
|
22
|
+
"pipelineStepStartConditions": {
|
|
23
|
+
"trigger": {
|
|
24
|
+
"type": "schedule",
|
|
25
|
+
"description": "Run daily at 09:00 — picks up everything sourced overnight.",
|
|
26
|
+
"config": { "frequency": "daily", "time": "09:00" }
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"next": { "stepId": "read-new" }
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"id": "read-new",
|
|
33
|
+
"type": "appAction",
|
|
34
|
+
"name": "Read Rows with status: new",
|
|
35
|
+
"app": { "id": "kg", "actionId": "kg.read-list", "source": "native" },
|
|
36
|
+
"stepInputData": {
|
|
37
|
+
"listKey": "{{input.list_key}}",
|
|
38
|
+
"status": "new",
|
|
39
|
+
"limit": "100"
|
|
40
|
+
},
|
|
41
|
+
"next": { "stepId": "process" }
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"id": "process",
|
|
45
|
+
"type": "aiAction",
|
|
46
|
+
"name": "Process Entity",
|
|
47
|
+
"loopConfig": {
|
|
48
|
+
"source": "executionContent",
|
|
49
|
+
"config": { "stepId": "read-new", "field": "listEntries" },
|
|
50
|
+
"ItemAlias": "row"
|
|
51
|
+
},
|
|
52
|
+
"pipelineStepPrompt": {
|
|
53
|
+
"template": "Process this entity. Replace this prompt with the real processing logic for your domain (enrich, score, classify, draft, etc.).\n\nEntity: {{row}}\n\nReturn the rowId verbatim plus any fields you want merged onto the KG row.",
|
|
54
|
+
"responseStructure": {
|
|
55
|
+
"rowId": "string — copy {{row.id}} verbatim so the update step can target this exact row",
|
|
56
|
+
"result": "object — fields to merge into the KG row (scores, decisions, derived data)"
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"creditCost": 5,
|
|
60
|
+
"next": { "stepId": "mark-processed" }
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"id": "mark-processed",
|
|
64
|
+
"type": "appAction",
|
|
65
|
+
"name": "Flip status -> processed",
|
|
66
|
+
"app": { "id": "kg", "actionId": "kg.update-rows", "source": "native" },
|
|
67
|
+
"entryConditions": {
|
|
68
|
+
"onCriteriaFail": "wait",
|
|
69
|
+
"conditionText": "Wait for the processing loop to finish before flipping status.",
|
|
70
|
+
"criteria": [
|
|
71
|
+
{ "type": "loop_completion", "stepId": "process", "operator": "==", "value": true }
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
"stepInputData": {
|
|
75
|
+
"listKey": "{{input.list_key}}",
|
|
76
|
+
"rowIds": "{{steps.process.rowIds}}",
|
|
77
|
+
"fieldUpdates": "{{steps.process.results}}",
|
|
78
|
+
"status": "processed"
|
|
79
|
+
},
|
|
80
|
+
"next": { "stepId": "done" }
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"id": "done",
|
|
84
|
+
"type": "milestone",
|
|
85
|
+
"name": "Processing Done"
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
}
|