@agentled/cli 0.7.6 → 0.7.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/README.md +18 -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/onboarding.js +43 -15
- package/dist/commands/onboarding.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/workspace-meta.d.ts +4 -3
- package/dist/utils/workspace-meta.js +27 -5
- package/dist/utils/workspace-meta.js.map +1 -1
- package/llms-full.txt +4 -2
- package/llms.txt +17 -0
- package/package.json +1 -1
- 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 +23 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace-meta.js","sourceRoot":"","sources":["../../src/utils/workspace-meta.ts"],"names":[],"mappings":"AAQA,SAAS,QAAQ,CAAC,KAAc;IAC5B,OAAO,CAAC,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AACzE,CAAC;AAED,SAAS,WAAW,CAAC,GAAG,MAAiB;IACrC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QACzB,IAAI,OAAO,KAAK,KAAK,QAAQ;YAAE,SAAS;QACxC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;QAC7B,IAAI,OAAO;YAAE,OAAO,OAAO,CAAC;IAChC,CAAC;IACD,OAAO,SAAS,CAAC;AACrB,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,KAAa;IAC7C,MAAM,IAAI,GAAG,KAAK;SACb,IAAI,EAAE;SACN,WAAW,EAAE;SACb,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;SACnB,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;SAC3B,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;IAE7B,OAAO,IAAI,IAAI,WAAW,CAAC;AAC/B,CAAC;AAED
|
|
1
|
+
{"version":3,"file":"workspace-meta.js","sourceRoot":"","sources":["../../src/utils/workspace-meta.ts"],"names":[],"mappings":"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"}
|
package/llms-full.txt
CHANGED
|
@@ -93,6 +93,8 @@ agentled workflows publish <id> --status paused
|
|
|
93
93
|
agentled workflows start <id> --input '{"company_url":"https://linkedin.com/company/acme"}'
|
|
94
94
|
agentled workflows start <id> --input-file input.json
|
|
95
95
|
|
|
96
|
+
`workflows start` always returns `executionInputId` (the submitted input/run record). It may also return `executionId` if the async execution row is already available. Use only `executionId` with `executions get`. If `executionId` is absent, run `agentled executions list <workflow-id> --limit 5` and find the row whose `pipelineExecutionInputId` matches the `executionInputId`; that row's `id` is the execution ID.
|
|
97
|
+
|
|
96
98
|
# Export / Import (cross-environment transfer)
|
|
97
99
|
agentled workflows export <id> --output workflow-export.json
|
|
98
100
|
agentled workflows import --file workflow-export.json
|
|
@@ -396,7 +398,7 @@ agentled workflows publish <id> --status live
|
|
|
396
398
|
agentled workflows start <id> --input '{"url":"https://linkedin.com/company/acme"}'
|
|
397
399
|
|
|
398
400
|
# Check results
|
|
399
|
-
agentled executions list <id> --limit
|
|
401
|
+
agentled executions list <id> --limit 5 # match pipelineExecutionInputId if start returned only executionInputId
|
|
400
402
|
agentled executions get <id> <exec-id>
|
|
401
403
|
```
|
|
402
404
|
|
|
@@ -495,7 +497,7 @@ agentled workflows start <workflow-id> --input '{"company_url": "$ARGUMENTS"}'
|
|
|
495
497
|
|
|
496
498
|
Check results with:
|
|
497
499
|
\`\`\`bash
|
|
498
|
-
agentled executions list <workflow-id> --limit
|
|
500
|
+
agentled executions list <workflow-id> --limit 5 # match pipelineExecutionInputId from workflows start
|
|
499
501
|
agentled executions get <workflow-id> <exec-id>
|
|
500
502
|
\`\`\`
|
|
501
503
|
```
|
package/llms.txt
CHANGED
|
@@ -29,3 +29,20 @@ to switch targets.
|
|
|
29
29
|
## Docs
|
|
30
30
|
|
|
31
31
|
- [Full Reference](llms-full.txt): Complete single-file reference for LLM consumption — all CLI commands, workflow schema, app actions, and examples
|
|
32
|
+
- [Design Principles](patterns/v1/00-design-principles.md): The four-pillar authoring contract — idempotency, small scope, KG-list-as-connector, design-as-data — read this **before authoring any workflow**.
|
|
33
|
+
|
|
34
|
+
## Scaffolds (zero-iteration starters)
|
|
35
|
+
|
|
36
|
+
Every scaffold is preflight-clean. List them with `agentled workflows scaffold --list`; copy one with `agentled workflows scaffold <slug> --out my.json`.
|
|
37
|
+
|
|
38
|
+
Idempotent sourcing + processing (Pattern 15 — the user's default for "find leads then process on a schedule"):
|
|
39
|
+
|
|
40
|
+
- `source-to-kg` — schedule → generate queries → extract → `kg.upsert-rows` with `userKey` + `status:new`. Re-runs dedup via userKey.
|
|
41
|
+
- `kg-process-update` — schedule → `kg.read-list(status:new)` → loop → `kg.update-rows(status:processed)`. Pair with `source-to-kg`.
|
|
42
|
+
|
|
43
|
+
Composition:
|
|
44
|
+
|
|
45
|
+
- `child-with-return` — child workflow with `internal:true` + `return` step. Called via `agentled.call-workflow`.
|
|
46
|
+
- `orchestrator-kg-loop` — orchestrator that loops `call-workflow` over a KG list, waits for `loop_completion`, persists results.
|
|
47
|
+
|
|
48
|
+
See `scaffolds/README.md` for the full catalog.
|
package/package.json
CHANGED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# 00 — Design Principles
|
|
2
|
+
|
|
3
|
+
> The four-pillar contract every Agentled workflow follows. Read this **before authoring any step.**
|
|
4
|
+
|
|
5
|
+
The other docs in this folder (`01-trigger-design`, `02-dedup-gates`, …) are tactical playbooks for specific shapes. This one is the contract underneath them: the four invariants every workflow you author has to satisfy, and the questions to answer before you write a single step.
|
|
6
|
+
|
|
7
|
+
If you remember nothing else from this folder, remember the four pillars and the three questions.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## The four pillars
|
|
12
|
+
|
|
13
|
+
### 1. Idempotency — re-running is safe
|
|
14
|
+
|
|
15
|
+
Every workflow you author must be re-runnable on the same input without producing duplicates, double-charges, or partial state. This is a property of the *workflow*, not the platform.
|
|
16
|
+
|
|
17
|
+
The two implementations:
|
|
18
|
+
|
|
19
|
+
- **Polling intake** — every trigger that reads from an external source (Gmail, Slack, a feed, a webhook replay) starts with a **dedup gate**. The default for email is a Gmail label gate (`-label:processed`) plus `GMAIL_ADD_LABEL` at the end of the loop. See [02-dedup-gates](02-dedup-gates.md).
|
|
20
|
+
- **KG writes** — use `kg.upsert-rows` with an explicit `userKey` (the LinkedIn URL, the domain, the order id — whatever is stable). Same `userKey` = same row across runs, O(1), no table scan. Reach for `kg.add-rows` only when duplicates are *acceptable*. See [12-event-driven-workflow-groups](12-event-driven-workflow-groups.md) §"Status is the API".
|
|
21
|
+
|
|
22
|
+
> **Test:** if you ran your workflow twice with the same input, would the world look the same as if you ran it once? If no, you have a missing dedup gate.
|
|
23
|
+
|
|
24
|
+
### 2. Small scope — one workflow owns one stage
|
|
25
|
+
|
|
26
|
+
A workflow that "sources leads, enriches them, scores them, drafts outreach, sends email, and updates the CRM" is not a workflow — it's a system that needs five. Symptoms of an over-scoped workflow:
|
|
27
|
+
|
|
28
|
+
- A failed step at minute 20 forces re-running steps 1–4 to retry step 5.
|
|
29
|
+
- The execution log spans 200+ rows and you can't tell which stage failed.
|
|
30
|
+
- Two upstream sources both want to feed in, but they can't share infrastructure.
|
|
31
|
+
- The "outreach" step needs different scheduling than the "sourcing" step.
|
|
32
|
+
|
|
33
|
+
The default decomposition rule: **one workflow per stage in the business process.** Stages are the things a human would describe with a different verb — *source*, *enrich*, *score*, *outreach*, *follow up*, *report*. Each stage gets its own workflow, its own trigger, its own retry semantics. See [05-child-workflow-contracts](05-child-workflow-contracts.md) for child workflows and [12-event-driven-workflow-groups](12-event-driven-workflow-groups.md) for multi-workflow systems.
|
|
34
|
+
|
|
35
|
+
> **Test:** can a human point at your workflow and say in one sentence what it does, without saying "and" twice? If not, split it.
|
|
36
|
+
|
|
37
|
+
### 3. KG list is the connector between workflows
|
|
38
|
+
|
|
39
|
+
When one workflow finishes and another begins, **the handoff is a Knowledge Graph list row, not a direct call**. The producing workflow writes (`kg.upsert-rows` with `status: "new"`); the consuming workflow reads (`kg.read-list` with `filters: { status: "new" }`) and updates (`kg.update-rows` to set `status: "processed"`).
|
|
40
|
+
|
|
41
|
+
Why a KG list and not `call-workflow`?
|
|
42
|
+
|
|
43
|
+
| KG-list handoff | Direct call |
|
|
44
|
+
|---|---|
|
|
45
|
+
| Producer doesn't know which consumers exist | Producer hardcodes consumer ID |
|
|
46
|
+
| Consumer can be paused, replaced, or duplicated freely | Pausing the consumer breaks the producer |
|
|
47
|
+
| Backfill = widen the status filter | Backfill = re-run the producer |
|
|
48
|
+
| State is durable and inspectable in the UI | State exists only in the execution log |
|
|
49
|
+
| Multiple producers can write to one list | Each producer wires up its own caller chain |
|
|
50
|
+
|
|
51
|
+
Use `call-workflow` (Pattern 11/12) when a child workflow is a **pure function** — same input always yields the same output, and the parent immediately consumes the return value. Use the KG list as connector for everything else.
|
|
52
|
+
|
|
53
|
+
> **Test:** if you swap one of your workflows for a different implementation tomorrow, do the others keep working without code changes? If no, you have a brittle direct-call dependency.
|
|
54
|
+
|
|
55
|
+
### 4. Design as data — persist decisions in KG text
|
|
56
|
+
|
|
57
|
+
The workflow JSON tells you *what* the workflow does. It does not tell you *why* it does it that way. Why this trigger? Why this scoring rubric? Why a dedup label and not a userKey? Without those answers, the next agent session — or the next human — rebuilds your reasoning from scratch every time.
|
|
58
|
+
|
|
59
|
+
Save the rationale **in the platform**, not in your local scratch folder. Two places, two purposes:
|
|
60
|
+
|
|
61
|
+
**Working memory (local, per-group)** — `workflow-groups/<group-slug>/design.md`, `decisions/`, `worklog.md`. The agent's scratch space while building. See [12-event-driven-workflow-groups](12-event-driven-workflow-groups.md) §"Folder layout".
|
|
62
|
+
|
|
63
|
+
**Durable memory (KG text, per-workspace)** — call the `upsert_knowledge_text` MCP tool when you reach a non-trivial decision. The decision survives across agent sessions, users, and CLI reinstalls. Example:
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
upsert_knowledge_text({
|
|
67
|
+
key: "design.<group-slug>.scoring-rubric",
|
|
68
|
+
value: "Scoring rubric for the <group> funnel.\n\nDimensions: team (40), traction (30), market (30).\nThreshold: score >= 70 → outreach; 50-69 → nurture; <50 → reject.\nWhy: validated against 200 hand-scored seed-stage rows. See decisions/0003.\nLast review: 2026-04."
|
|
69
|
+
})
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Use the `design.<group-slug>.<topic>` key convention so a later agent can `recall_memory({ key: "design.<group-slug>.scoring-rubric" })` or `search_memories({ query: "scoring rubric", scope: "workspace" })` without guessing.
|
|
73
|
+
|
|
74
|
+
What to upsert as `design.*` text:
|
|
75
|
+
|
|
76
|
+
| Topic | Key suffix | What to capture |
|
|
77
|
+
|-------|-----------|-----------------|
|
|
78
|
+
| Group goal & operating principles | `.charter` | The 1-paragraph goal and the 3–5 operating principles |
|
|
79
|
+
| ICP / scoring rubric | `.scoring-rubric` | Dimensions, weights, thresholds, the data the thresholds were calibrated on |
|
|
80
|
+
| KG list shapes | `.kg-lists` | List keys, `userKeyField`, state machines, who reads/writes |
|
|
81
|
+
| Approval gate policies | `.approvals` | Which steps gate, who approves, escalation rules |
|
|
82
|
+
| Channel / outreach voice | `.outreach-voice` | Tone, forbidden phrases, brand rules |
|
|
83
|
+
| Rejected alternatives | `.tradeoffs` | What you considered and didn't ship, and why |
|
|
84
|
+
|
|
85
|
+
> **Test:** if a new agent picks up this group tomorrow with no chat history, can it answer "what is the rubric and why does it look like this?" by reading what's in KG text? If no, you owe a `design.*` upsert.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## The three questions before you author
|
|
90
|
+
|
|
91
|
+
Answer all three out loud (or in `design.md`) before the first `add_step`. If you can't answer, you don't have enough context — orient the workspace first (`agentled workspace inspect`, `list_workflows`, `list_knowledge_lists`).
|
|
92
|
+
|
|
93
|
+
1. **What is the unit of work?** One company? One lead? One inbound email? One edition? The unit determines the loop scope, the KG row, the `userKey`, and the retry granularity.
|
|
94
|
+
2. **What is the completion signal?** A KG row status flip? A downstream trigger firing? A human approval? A milestone with no successor? "Completion" must be a thing the platform can observe — not an intent in the agent's head.
|
|
95
|
+
3. **Is this workflow part of a group?** If the user describes more than one verb ("source *and* enrich", "enrich *and* outreach"), the answer is yes and you owe a group manifest before you write step 1. See [12-event-driven-workflow-groups](12-event-driven-workflow-groups.md).
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Decision tree: which shape do I author?
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
Is the user describing one verb, one input, one output?
|
|
103
|
+
├─ YES, and it's truly one-shot → Single workflow, milestone end.
|
|
104
|
+
│ Scaffolds: minimal, ai-with-tools, list-match-email.
|
|
105
|
+
│
|
|
106
|
+
├─ YES, but the same logic will be reused by other workflows → Child workflow.
|
|
107
|
+
│ Terminal step is `return`, not `milestone`.
|
|
108
|
+
│ Scaffold: child-with-return.
|
|
109
|
+
│ Pattern: 05.
|
|
110
|
+
│
|
|
111
|
+
└─ NO, the user is describing a process with multiple stages →
|
|
112
|
+
│
|
|
113
|
+
├─ Stages share state and run on different cadences → Two (or more) workflows
|
|
114
|
+
│ connected by a KG list. Producer upserts with status: "new"; consumer
|
|
115
|
+
│ reads by status, processes, updates status.
|
|
116
|
+
│ Scaffolds: source-to-kg + kg-process-update.
|
|
117
|
+
│ Pattern: 12, 15.
|
|
118
|
+
│
|
|
119
|
+
└─ One workflow needs to fan out the same operation per item with deterministic
|
|
120
|
+
results → Orchestrator + child via call-workflow inside a loop.
|
|
121
|
+
Scaffold: orchestrator-kg-loop.
|
|
122
|
+
Pattern: 11.
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## The 30-second sanity check
|
|
128
|
+
|
|
129
|
+
Before you publish, walk this checklist. Each "no" is a blocker.
|
|
130
|
+
|
|
131
|
+
- [ ] If this workflow is re-run on the same input, does it produce the same state? (Pillar 1)
|
|
132
|
+
- [ ] Can I describe what this workflow does in one short sentence without "and"? (Pillar 2)
|
|
133
|
+
- [ ] If the next workflow in the chain is paused or replaced, does this one still do its job? (Pillar 3)
|
|
134
|
+
- [ ] Are the design rationale, ICP/rubric, and list shapes saved as `design.<group>.*` in KG text? (Pillar 4)
|
|
135
|
+
- [ ] Did I answer the three questions before writing step 1?
|
|
136
|
+
- [ ] Did I pick the shape from the decision tree, not invent a new one?
|
|
137
|
+
- [ ] Does the workflow pass `agentled workflows validate <wfId>` with zero errors and zero warnings?
|
|
138
|
+
- [ ] Did I run the [Dry-Run Protocol](../../../CLAUDE.md) (`validate_workflow` → `test_*_action` per key step → check `{{steps.X.field}}` refs against captured payloads) before any `start_workflow`?
|
|
139
|
+
|
|
140
|
+
If all eight pass, ship it. If not, fix before publish — the platform won't catch a missing rationale or a misnamed list key for you.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Where to go next
|
|
145
|
+
|
|
146
|
+
| If you're doing… | Read |
|
|
147
|
+
|---|---|
|
|
148
|
+
| Choosing a trigger | [01-trigger-design](01-trigger-design.md) |
|
|
149
|
+
| Building a polling intake | [02-dedup-gates](02-dedup-gates.md) |
|
|
150
|
+
| Iterating without burning credits | [03-credit-efficiency](03-credit-efficiency.md) |
|
|
151
|
+
| Looping over a KG list | [04-loop-patterns](04-loop-patterns.md) |
|
|
152
|
+
| Building a child workflow | [05-child-workflow-contracts](05-child-workflow-contracts.md) |
|
|
153
|
+
| Conditional routing | [06-conditional-routing](06-conditional-routing.md) |
|
|
154
|
+
| Handling errors / skips / waits | [07-error-handling](07-error-handling.md) |
|
|
155
|
+
| Sending email with approval | [08-composed-email-approval](08-composed-email-approval.md) |
|
|
156
|
+
| Rendering reports + KG storage | [09-reports-and-knowledge-storage](09-reports-and-knowledge-storage.md) |
|
|
157
|
+
| Researching a person | [10-person-research-ladder](10-person-research-ladder.md) |
|
|
158
|
+
| Researching a company | [11-company-research-ladder](11-company-research-ladder.md) |
|
|
159
|
+
| Designing a multi-workflow group | [12-event-driven-workflow-groups](12-event-driven-workflow-groups.md) |
|
|
160
|
+
|
|
161
|
+
And before you start: `agentled workflows scaffold --list` to see every starter shape, then `agentled workflows scaffold <slug> --out my-pipeline.json` to bootstrap.
|
|
@@ -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
|
+
}
|