@agentled/cli 0.1.5 → 0.4.3
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 +136 -0
- package/dist/commands/auth.js +30 -0
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/examples.d.ts +15 -0
- package/dist/commands/examples.js +100 -0
- package/dist/commands/examples.js.map +1 -0
- package/dist/commands/scaffold.d.ts +14 -0
- package/dist/commands/scaffold.js +103 -0
- package/dist/commands/scaffold.js.map +1 -0
- package/dist/commands/schema.d.ts +10 -0
- package/dist/commands/schema.js +58 -0
- package/dist/commands/schema.js.map +1 -0
- package/dist/commands/skills.d.ts +9 -0
- package/dist/commands/skills.js +94 -0
- package/dist/commands/skills.js.map +1 -0
- package/dist/commands/workflows.js +227 -9
- package/dist/commands/workflows.js.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/utils/preflight.d.ts +25 -0
- package/dist/utils/preflight.js +185 -0
- package/dist/utils/preflight.js.map +1 -0
- package/dist/utils/skills.d.ts +49 -0
- package/dist/utils/skills.js +214 -0
- package/dist/utils/skills.js.map +1 -0
- package/package.json +4 -1
- package/patterns/v1/00-why-agentic-ops.md +107 -0
- package/patterns/v1/01-trigger-design.md +107 -0
- package/patterns/v1/02-dedup-gates.md +135 -0
- package/patterns/v1/03-credit-efficiency.md +130 -0
- package/patterns/v1/04-loop-patterns.md +147 -0
- package/patterns/v1/05-child-workflow-contracts.md +151 -0
- package/patterns/v1/06-conditional-routing.md +151 -0
- package/patterns/v1/07-error-handling.md +157 -0
- package/patterns/v1/08-composed-email-approval.md +130 -0
- package/patterns/v1/09-reports-and-knowledge-storage.md +166 -0
- package/scaffolds/README.md +61 -0
- package/scaffolds/email-polling-dedup.json +71 -0
- package/scaffolds/extract-threshold-alert.json +131 -0
- package/scaffolds/lead-scoring-kg.json +84 -0
- package/scaffolds/list-match-email.json +131 -0
- package/scaffolds/minimal.json +20 -0
- package/skills/agentled/SKILL.md +568 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# 06 — Conditional routing: conditions that actually fire
|
|
2
|
+
|
|
3
|
+
**Problem**: Entry conditions on workflow steps silently fail to apply — steps run unconditionally, or skip unconditionally — because the wrong field names are used to configure them.
|
|
4
|
+
|
|
5
|
+
**Why it fails silently**: The wrong field names (`conditions` instead of `criteria`, `field` instead of `variable`) are not validation errors. The platform silently ignores unrecognized fields and applies no condition at all. The step runs every time, or skips every time, with no visible indication that the condition isn't being evaluated.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## The field name trap
|
|
10
|
+
|
|
11
|
+
This is the most subtle silent failure in workflow configuration. Two pairs of field names look interchangeable but aren't:
|
|
12
|
+
|
|
13
|
+
| Wrong | Correct |
|
|
14
|
+
|---|---|
|
|
15
|
+
| `conditions` | `criteria` |
|
|
16
|
+
| `field` | `variable` |
|
|
17
|
+
|
|
18
|
+
```yaml
|
|
19
|
+
# Wrong: unrecognized field names — condition is silently ignored
|
|
20
|
+
entryConditions:
|
|
21
|
+
onCriteriaFail: "skip"
|
|
22
|
+
conditions: # ← wrong: should be "criteria"
|
|
23
|
+
- field: "{{input.score}}" # ← wrong: should be "variable"
|
|
24
|
+
operator: ">"
|
|
25
|
+
value: 70
|
|
26
|
+
|
|
27
|
+
# Correct: recognized field names — condition is evaluated
|
|
28
|
+
entryConditions:
|
|
29
|
+
onCriteriaFail: "skip"
|
|
30
|
+
criteria: # ← correct
|
|
31
|
+
- variable: "{{input.score}}" # ← correct
|
|
32
|
+
operator: ">"
|
|
33
|
+
value: 70
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The wrong version doesn't error. The step runs unconditionally, as if no condition existed.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Anti-pattern
|
|
41
|
+
|
|
42
|
+
```yaml
|
|
43
|
+
# Wrong: routes HOT leads to Slack, but the condition is silently ignored
|
|
44
|
+
- id: notify-hot-lead
|
|
45
|
+
type: app-action
|
|
46
|
+
app: slack
|
|
47
|
+
entryConditions:
|
|
48
|
+
onCriteriaFail: "skip"
|
|
49
|
+
conditions: # ← wrong field name
|
|
50
|
+
- field: "{{steps.score.category}}" # ← wrong field name
|
|
51
|
+
operator: "=="
|
|
52
|
+
value: "HOT"
|
|
53
|
+
# Result: every lead triggers a Slack notification, including COLD and WARM
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Correct pattern
|
|
59
|
+
|
|
60
|
+
```yaml
|
|
61
|
+
# Correct: HOT leads only
|
|
62
|
+
- id: notify-hot-lead
|
|
63
|
+
type: app-action
|
|
64
|
+
app: slack
|
|
65
|
+
entryConditions:
|
|
66
|
+
onCriteriaFail: "skip"
|
|
67
|
+
conditionText: "Only notify for HOT leads"
|
|
68
|
+
criteria: # ← correct
|
|
69
|
+
- variable: "{{steps.score.category}}" # ← correct
|
|
70
|
+
operator: "=="
|
|
71
|
+
value: "HOT"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## How to verify a condition is actually firing
|
|
77
|
+
|
|
78
|
+
When a condition behaves unexpectedly (step always runs, or always skips), check:
|
|
79
|
+
|
|
80
|
+
1. **Field names**: is it `criteria`/`variable`? Not `conditions`/`field`?
|
|
81
|
+
2. **Variable path**: does `{{steps.score.category}}` actually resolve? Check the referenced step's output in execution logs.
|
|
82
|
+
3. **Value type**: comparing a string `"70"` to a number `70` with `>` may not work as expected — check that types match.
|
|
83
|
+
4. **Null safety**: `isNotNull` checks should come before value comparisons to avoid null reference issues.
|
|
84
|
+
|
|
85
|
+
```yaml
|
|
86
|
+
# Pattern: null-safe condition chain
|
|
87
|
+
criteria:
|
|
88
|
+
- variable: "{{steps.enrich.company}}"
|
|
89
|
+
operator: "isNotNull" # check existence first
|
|
90
|
+
- variable: "{{steps.score.total}}"
|
|
91
|
+
operator: ">"
|
|
92
|
+
value: 70 # then compare value
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## The LLM-as-router anti-pattern
|
|
98
|
+
|
|
99
|
+
Using an AI step to make a binary routing decision that a simple condition can handle:
|
|
100
|
+
|
|
101
|
+
```yaml
|
|
102
|
+
# Wrong: burning 10 credits to decide yes/no
|
|
103
|
+
- id: decide-routing
|
|
104
|
+
type: ai-action
|
|
105
|
+
creditCost: 10
|
|
106
|
+
prompt: |
|
|
107
|
+
Given this lead's score of {{steps.score.value}}, should we send a Slack alert?
|
|
108
|
+
Score above 80 means yes. Below 80 means no.
|
|
109
|
+
Return: { shouldAlert: boolean }
|
|
110
|
+
|
|
111
|
+
- id: send-alert
|
|
112
|
+
entryConditions:
|
|
113
|
+
criteria:
|
|
114
|
+
- variable: "{{steps.decide-routing.shouldAlert}}"
|
|
115
|
+
operator: "=="
|
|
116
|
+
value: true
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
```yaml
|
|
120
|
+
# Correct: free condition, no LLM call needed
|
|
121
|
+
- id: send-alert
|
|
122
|
+
entryConditions:
|
|
123
|
+
onCriteriaFail: "skip"
|
|
124
|
+
conditionText: "Only alert for high-score leads"
|
|
125
|
+
criteria:
|
|
126
|
+
- variable: "{{steps.score.value}}"
|
|
127
|
+
operator: ">"
|
|
128
|
+
value: 80
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Use AI for decisions that require reasoning, judgment, or context. Use conditions for decisions that follow a deterministic rule.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## `onCriteriaFail` reference
|
|
136
|
+
|
|
137
|
+
| Value | Behavior |
|
|
138
|
+
|---|---|
|
|
139
|
+
| `"skip"` | Skip this step, continue to the next |
|
|
140
|
+
| `"stop"` | Stop the entire execution |
|
|
141
|
+
| `"wait"` | Block this step until criteria are met (used for loop/group completion) |
|
|
142
|
+
|
|
143
|
+
Common mistakes:
|
|
144
|
+
- Using `"stop"` when you mean `"skip"` — stops the whole workflow instead of just bypassing one step
|
|
145
|
+
- Using `"skip"` when you mean `"wait"` — the step runs immediately with incomplete data instead of waiting for a loop to finish
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## One-line rule
|
|
150
|
+
|
|
151
|
+
> Always use `criteria` (not `conditions`) and `variable` (not `field`) in entry conditions — wrong field names are silently ignored and the condition never evaluates.
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# 07 — Error handling: skip vs stop vs wait
|
|
2
|
+
|
|
3
|
+
**Problem**: Developers use the same error behavior (`stop` or `skip`) everywhere, causing workflows to either halt on recoverable errors or continue silently with missing data.
|
|
4
|
+
|
|
5
|
+
**Why it fails silently**: `skip` silently passes empty/null data to downstream steps, which then produce wrong outputs with no error. `stop` halts workflows on optional steps that should have been bypassed. Neither produces a visible failure at the wrong step — the failure surfaces later, in a confusing place.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## The three behaviors
|
|
10
|
+
|
|
11
|
+
Every entry condition has an `onCriteriaFail` setting that determines what happens when the condition isn't met:
|
|
12
|
+
|
|
13
|
+
| Value | What it does | When to use |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| `skip` | Skip this step, pass `null`/empty to downstream steps, continue execution | Optional step — workflow is valid without it |
|
|
16
|
+
| `stop` | Halt the entire execution immediately | Hard prerequisite — nothing downstream is meaningful without this step |
|
|
17
|
+
| `wait` | Block this step until criteria become true | Async dependency — waiting for a loop or parallel group to finish |
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Anti-pattern: stop everywhere
|
|
22
|
+
|
|
23
|
+
```yaml
|
|
24
|
+
# Wrong: using stop for an optional enrichment step
|
|
25
|
+
- id: enrich-linkedin
|
|
26
|
+
type: app-action
|
|
27
|
+
entryConditions:
|
|
28
|
+
onCriteriaFail: "stop" # ← wrong: this is optional
|
|
29
|
+
criteria:
|
|
30
|
+
- variable: "{{input.linkedinUrl}}"
|
|
31
|
+
operator: "isNotNull"
|
|
32
|
+
# If linkedinUrl is missing: entire workflow halts.
|
|
33
|
+
# But the workflow could run fine without LinkedIn data.
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```yaml
|
|
37
|
+
# Correct: optional enrichment step uses skip
|
|
38
|
+
- id: enrich-linkedin
|
|
39
|
+
type: app-action
|
|
40
|
+
entryConditions:
|
|
41
|
+
onCriteriaFail: "skip" # ← correct: skip if no URL
|
|
42
|
+
conditionText: "Skip LinkedIn enrichment if no URL provided"
|
|
43
|
+
criteria:
|
|
44
|
+
- variable: "{{input.linkedinUrl}}"
|
|
45
|
+
operator: "isNotNull"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Anti-pattern: skip for hard prerequisites
|
|
51
|
+
|
|
52
|
+
```yaml
|
|
53
|
+
# Wrong: using skip for required input validation
|
|
54
|
+
- id: validate-required-fields
|
|
55
|
+
type: code
|
|
56
|
+
entryConditions:
|
|
57
|
+
onCriteriaFail: "skip" # ← wrong: nothing downstream works without this
|
|
58
|
+
criteria:
|
|
59
|
+
- variable: "{{input.companyDomain}}"
|
|
60
|
+
operator: "isNotNull"
|
|
61
|
+
# If domain is missing: validation is skipped.
|
|
62
|
+
# Next step tries to enrich with null domain.
|
|
63
|
+
# Enrichment fails with a confusing error 3 steps later.
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```yaml
|
|
67
|
+
# Correct: hard prerequisite uses stop
|
|
68
|
+
- id: validate-required-fields
|
|
69
|
+
type: code
|
|
70
|
+
entryConditions:
|
|
71
|
+
onCriteriaFail: "stop" # ← correct: stop early with a clear failure
|
|
72
|
+
conditionText: "Company domain is required"
|
|
73
|
+
criteria:
|
|
74
|
+
- variable: "{{input.companyDomain}}"
|
|
75
|
+
operator: "isNotNull"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Fail fast. A clear stop at the prerequisite is better than a confusing error 5 steps later.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## `wait`: blocking on async completion
|
|
83
|
+
|
|
84
|
+
`wait` is for a specific use case: blocking a step until an async process finishes. The two common cases are loop completion and parallel group completion.
|
|
85
|
+
|
|
86
|
+
```yaml
|
|
87
|
+
# Wait for a loop to finish before consuming its output
|
|
88
|
+
- id: aggregate-results
|
|
89
|
+
type: ai-action
|
|
90
|
+
entryConditions:
|
|
91
|
+
onCriteriaFail: "wait"
|
|
92
|
+
conditionText: "Wait for all enrichment loop iterations to complete"
|
|
93
|
+
criteria:
|
|
94
|
+
- type: loop_completion
|
|
95
|
+
stepId: enrich-loop
|
|
96
|
+
operator: "=="
|
|
97
|
+
value: true
|
|
98
|
+
prompt: "Summarize these enriched companies: {{steps.enrich-loop.outputs}}"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`wait` is not a general-purpose retry mechanism. It's specifically for dependency ordering in async workflows.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Decision guide
|
|
106
|
+
|
|
107
|
+
Ask these questions in order:
|
|
108
|
+
|
|
109
|
+
**1. Is this step required for the workflow to produce a valid output?**
|
|
110
|
+
- Yes → `stop`
|
|
111
|
+
- No → continue to question 2
|
|
112
|
+
|
|
113
|
+
**2. Is this step waiting for another step/loop/group to finish?**
|
|
114
|
+
- Yes → `wait`
|
|
115
|
+
- No → `skip`
|
|
116
|
+
|
|
117
|
+
Examples:
|
|
118
|
+
|
|
119
|
+
| Step | Required? | Async wait? | Use |
|
|
120
|
+
|---|---|---|---|
|
|
121
|
+
| "Validate required domain field" | Yes | No | `stop` |
|
|
122
|
+
| "Enrich LinkedIn (optional)" | No | No | `skip` |
|
|
123
|
+
| "Send Slack alert (if webhook configured)" | No | No | `skip` |
|
|
124
|
+
| "Aggregate loop results" | Yes | Yes (loop) | `wait` |
|
|
125
|
+
| "Score company (ICP match required)" | Yes | No | `stop` |
|
|
126
|
+
| "Add CRM note (nice to have)" | No | No | `skip` |
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Handling downstream null data after skip
|
|
131
|
+
|
|
132
|
+
When a step is skipped, its outputs are `null` or empty. Downstream steps that reference those outputs must handle nulls gracefully:
|
|
133
|
+
|
|
134
|
+
```yaml
|
|
135
|
+
# Pattern: null-safe reference in AI prompt
|
|
136
|
+
prompt: |
|
|
137
|
+
Company: {{steps.enrich.company.name | default: "Unknown"}}
|
|
138
|
+
LinkedIn headline: {{steps.enrich-linkedin.headline | default: "Not available"}}
|
|
139
|
+
Score this company based on what's available.
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Or use a code step to normalize before passing to downstream AI steps:
|
|
143
|
+
|
|
144
|
+
```javascript
|
|
145
|
+
// Code step: normalize potentially-null enrichment data
|
|
146
|
+
return {
|
|
147
|
+
name: input.enrichData?.name ?? input.rawInput.companyName,
|
|
148
|
+
industry: input.enrichData?.industry ?? "Unknown",
|
|
149
|
+
employees: input.enrichData?.employees ?? null,
|
|
150
|
+
};
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## One-line rule
|
|
156
|
+
|
|
157
|
+
> Use `skip` for optional steps, `stop` for hard prerequisites, and `wait` for async dependencies — and always handle `null` outputs from skipped steps in downstream steps.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# 08 — Composed email with approval gate: outreach that ships safely
|
|
2
|
+
|
|
3
|
+
**Problem**: Agents build outreach by chaining a "draft email" AI step with a separate "gmail send" app action. This skips the user-review step, sends unreviewed drafts, and can't display the email in the pending-approval UI.
|
|
4
|
+
|
|
5
|
+
**Why it fails silently**: A raw draft-then-send pipeline has no concept of "email" as a first-class artifact. The orchestrator's approval UI won't render a preview, the `schedule-email` action can't fire, and there's no audit trail of what was sent to whom. The workflow looks correct at authoring time and may even work in testing — until a real recipient gets an unreviewed draft.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## The composed-email shape
|
|
10
|
+
|
|
11
|
+
An email step is a specialized `aiAction` with four coupled pieces:
|
|
12
|
+
|
|
13
|
+
1. **Prompt type**: `pipelineStepPrompt.type: "email"` — tells the orchestrator this is an email step (not a generic AI step).
|
|
14
|
+
2. **Renderer**: `renderer: { type: "Email", config: { fromContextKey: "outreachProfile" } }` — tells the UI to render the result as an email preview, pulling sender info from a context input page.
|
|
15
|
+
3. **Integrations declaration**: declares that the workflow needs a connected email account (Gmail or Outlook).
|
|
16
|
+
4. **Approval + send action**: `onApproval: { action: "schedule-email" }` + `next.conditions.approvalRequired: true` — the step waits for user approval, then the `schedule-email` action actually sends the email.
|
|
17
|
+
|
|
18
|
+
All four must be present. Omit any one and the step silently degrades: no preview, no approval gate, or no send.
|
|
19
|
+
|
|
20
|
+
## Required input page: outreachProfile
|
|
21
|
+
|
|
22
|
+
Sender identity lives on a workflow-level `inputPages` entry with `contextKey: "outreachProfile"`. Without this page, the `{{context.outreachProfile.fromEmail}}` template resolves to empty and the email has no sender.
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"title": "Outreach Profile",
|
|
27
|
+
"pathname": "outreach-profile",
|
|
28
|
+
"configuration": {
|
|
29
|
+
"contextKey": "outreachProfile",
|
|
30
|
+
"shortDescriptionFields": ["name", "fromEmail"],
|
|
31
|
+
"fields": [
|
|
32
|
+
{ "name": "name", "label": "Sender name", "type": "text", "required": true },
|
|
33
|
+
{ "name": "fromEmailLabel", "label": "From name", "type": "text", "required": true },
|
|
34
|
+
{ "name": "fromEmail", "label": "From email", "type": "connected_emails_selector_multiple", "required": true },
|
|
35
|
+
{ "name": "replyToEmail", "label": "Reply-to (optional)", "type": "text" }
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Full composed-email step (copy-paste)
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"id": "send-outreach-email",
|
|
46
|
+
"type": "aiAction",
|
|
47
|
+
"name": "Send Outreach Email",
|
|
48
|
+
"pipelineStepPrompt": {
|
|
49
|
+
"type": "email",
|
|
50
|
+
"template": "Draft a personalized email to {{steps.enrich.contact.fullName}} at {{steps.enrich.company.name}} introducing our offering. Keep it warm, concise, and specific to their role.",
|
|
51
|
+
"responseStructure": {
|
|
52
|
+
"email": {
|
|
53
|
+
"from": "{{context.outreachProfile.fromEmail}}",
|
|
54
|
+
"to": "{{steps.enrich.contact.email}}",
|
|
55
|
+
"subject": "",
|
|
56
|
+
"body": "",
|
|
57
|
+
"bodyType": "html"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"renderer": {
|
|
62
|
+
"type": "Email",
|
|
63
|
+
"config": { "fromContextKey": "outreachProfile" }
|
|
64
|
+
},
|
|
65
|
+
"integrations": [
|
|
66
|
+
{
|
|
67
|
+
"type": "oneOf",
|
|
68
|
+
"label": "Email",
|
|
69
|
+
"connectorType": "email",
|
|
70
|
+
"options": [
|
|
71
|
+
{ "name": "Gmail", "url": "https://gmail.com", "isUserAccountConnectionRequired": true },
|
|
72
|
+
{ "name": "Outlook", "url": "https://outlook.com", "isUserAccountConnectionRequired": true }
|
|
73
|
+
],
|
|
74
|
+
"selectionHint": "preferConnected"
|
|
75
|
+
}
|
|
76
|
+
],
|
|
77
|
+
"onApproval": {
|
|
78
|
+
"executedText": "Email sent by {{name}} at {{date}}",
|
|
79
|
+
"failedText": "Email failed to send.",
|
|
80
|
+
"action": "schedule-email"
|
|
81
|
+
},
|
|
82
|
+
"creditCost": 5,
|
|
83
|
+
"next": { "stepId": "done", "conditions": { "approvalRequired": true } }
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Anti-patterns
|
|
88
|
+
|
|
89
|
+
**Draft + Gmail send (no approval):**
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
❌ draft-email (aiAction) → gmail.send-email (appAction) → done
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
No preview, no user approval, and the drafted body is written as a plain string instead of the structured `email` object — Gmail's send-email action expects specific fields (`to`, `subject`, `html`) that the draft prompt has no way to produce reliably.
|
|
96
|
+
|
|
97
|
+
**Missing `pipelineStepPrompt.type: "email"`:**
|
|
98
|
+
|
|
99
|
+
```yaml
|
|
100
|
+
pipelineStepPrompt:
|
|
101
|
+
template: "..."
|
|
102
|
+
responseStructure:
|
|
103
|
+
email: { from, to, subject, body, bodyType }
|
|
104
|
+
# ❌ No `type: "email"` — the renderer falls back to a generic JSON view,
|
|
105
|
+
# and the orchestrator treats this as a regular aiAction (no schedule-email).
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Email body as plain text when `bodyType: "html"`:**
|
|
109
|
+
|
|
110
|
+
Email clients render HTML bodies literally if the wrapping element is missing. Always generate email-safe HTML (`<p>`, `<br>`, `<a>`, `<strong>` — no CSS blocks, no `<script>`). Don't use markdown — the approval UI won't convert it.
|
|
111
|
+
|
|
112
|
+
**No `outreachProfile` input page:**
|
|
113
|
+
|
|
114
|
+
`{{context.outreachProfile.fromEmail}}` resolves to an empty string. The email has no sender. It may still send (from the default connected account) but you lose per-workflow sender control.
|
|
115
|
+
|
|
116
|
+
## Checklist
|
|
117
|
+
|
|
118
|
+
Before wiring a composed-email step into a workflow:
|
|
119
|
+
|
|
120
|
+
- [ ] `context.inputPages[]` contains the `outreachProfile` page (pattern above)
|
|
121
|
+
- [ ] `pipelineStepPrompt.type: "email"` is set
|
|
122
|
+
- [ ] `renderer: { type: "Email", config: { fromContextKey: "outreachProfile" } }` is present
|
|
123
|
+
- [ ] `integrations[]` declares the email connector (Gmail / Outlook)
|
|
124
|
+
- [ ] `onApproval.action: "schedule-email"` is set
|
|
125
|
+
- [ ] `next.conditions.approvalRequired: true` on the outgoing edge
|
|
126
|
+
- [ ] Recipient and subject are derived from prior step output, not hardcoded
|
|
127
|
+
|
|
128
|
+
## When to use a generic approval gate instead
|
|
129
|
+
|
|
130
|
+
For non-email approvals (sending a Slack post, firing a webhook), use a plain `aiAction` + `next.conditions.approvalRequired: true` without the email renderer. The approval gate is a general-purpose feature; the email shape is only required when the approved artifact is an email that needs to be sent through the user's connected mailbox.
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# 09 — Reports, sharing, and knowledge-graph storage: closing the loop
|
|
2
|
+
|
|
3
|
+
**Problem**: Agents produce analysis as free-form prose in AI step output, then either never display it or drop it into a raw JSON view. Results aren't shareable with stakeholders, aren't comparable across runs, and don't feed back into the Knowledge Graph for trend analysis or future scoring calibration.
|
|
4
|
+
|
|
5
|
+
**Why it fails silently**: Without a `renderer`, an AI step's structured output is unreadable in the workspace UI. Without a `share` step, the output has no URL that can be sent to a stakeholder. Without a `knowledgeSync` step, each run's data is discarded — the second run can't learn from the first, and KPIs have no history to trend against.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## The three-part closing loop
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
... upstream steps → AI report step (with Config renderer)
|
|
13
|
+
→ [optional] share step (public URL)
|
|
14
|
+
→ [optional] composed email step (delivery)
|
|
15
|
+
→ knowledgeSync (persist to KG for trending)
|
|
16
|
+
→ milestone
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The three pieces are independent and optional, but they compound. A report with a renderer is readable. A report with a renderer + share step is forwardable. A report with all three + knowledgeSync is how KPI dashboards actually get built.
|
|
20
|
+
|
|
21
|
+
## 1. Report AI step with Config renderer
|
|
22
|
+
|
|
23
|
+
The Config renderer turns structured AI output into a KPI dashboard view. Key blocks: `kpiRow`, `markdown`, `table`, `signalList`.
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"id": "generate-report",
|
|
28
|
+
"type": "aiAction",
|
|
29
|
+
"name": "Generate Report",
|
|
30
|
+
"pipelineStepPrompt": {
|
|
31
|
+
"template": "Analyze the data and produce a structured report.\n\nData: {{steps.evaluate.items}}",
|
|
32
|
+
"responseStructure": {
|
|
33
|
+
"summary": "string — executive summary",
|
|
34
|
+
"kpis": "object { total: number, qualified: number, avgScore: number }",
|
|
35
|
+
"items": "array of { name, score, decision }",
|
|
36
|
+
"insights": "array of strings"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"renderer": {
|
|
40
|
+
"type": "Config",
|
|
41
|
+
"config": {
|
|
42
|
+
"layout": {
|
|
43
|
+
"title": "Run Report",
|
|
44
|
+
"blocks": [
|
|
45
|
+
{ "blockType": "kpiRow", "kpis": [
|
|
46
|
+
{ "label": "Total", "valuePath": "kpis.total", "icon": "Hash" },
|
|
47
|
+
{ "label": "Qualified", "valuePath": "kpis.qualified", "icon": "CheckCircle" },
|
|
48
|
+
{ "label": "Avg Score", "valuePath": "kpis.avgScore", "format": "score" }
|
|
49
|
+
]},
|
|
50
|
+
{ "blockType": "markdown", "contentPath": "summary" },
|
|
51
|
+
{ "blockType": "table", "arrayPath": "items", "searchable": true, "columns": [
|
|
52
|
+
{ "header": "Name", "field": "name" },
|
|
53
|
+
{ "header": "Score", "field": "score", "display": "score", "sortable": true },
|
|
54
|
+
{ "header": "Decision", "field": "decision", "display": "badge" }
|
|
55
|
+
]},
|
|
56
|
+
{ "blockType": "signalList", "title": "Insights", "arrayPath": "insights", "variant": "signal" }
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"creditCost": 10,
|
|
62
|
+
"next": { "stepId": "share-report" }
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Rule**: the `responseStructure` keys must match the renderer's `valuePath`/`arrayPath`/`contentPath` references. Drift between the two silently produces empty KPI tiles and blank tables.
|
|
67
|
+
|
|
68
|
+
## 2. Share step for a public URL
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"id": "share-report",
|
|
73
|
+
"type": "share",
|
|
74
|
+
"name": "Create Public Report Link",
|
|
75
|
+
"shareConfig": {
|
|
76
|
+
"outputSteps": ["generate-report"],
|
|
77
|
+
"expiresInDays": 30,
|
|
78
|
+
"visibility": "public"
|
|
79
|
+
},
|
|
80
|
+
"next": { "stepId": "send-report-email" }
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Outputs `{ shareId, shareUrl, expiresAt }`. Downstream steps use `{{steps.share-report.shareUrl}}` to reference the public URL (e.g., in an email body).
|
|
85
|
+
|
|
86
|
+
**When to add a share step**:
|
|
87
|
+
|
|
88
|
+
- Anyone outside the workspace needs to see the report.
|
|
89
|
+
- The report should remain accessible after the execution's detail page expires.
|
|
90
|
+
- The workflow delivers the report via email and the body should link to the full report.
|
|
91
|
+
|
|
92
|
+
**When to skip it**: internal-only dashboards where viewers already have workspace access.
|
|
93
|
+
|
|
94
|
+
## 3. Knowledge-graph storage for trending
|
|
95
|
+
|
|
96
|
+
Without this, each run's data vanishes. With it, you can:
|
|
97
|
+
|
|
98
|
+
- Compare "today's MRR" against last month's.
|
|
99
|
+
- Calibrate future AI scoring on past outcomes (see `kg.retrieve-scoring-memory`).
|
|
100
|
+
- Build a timeline view of any metric.
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"id": "store-history",
|
|
105
|
+
"type": "knowledgeSync",
|
|
106
|
+
"name": "Store to KPI History",
|
|
107
|
+
"knowledgeSync": {
|
|
108
|
+
"source": { "stepId": "generate-report" },
|
|
109
|
+
"listKey": "kpi_history",
|
|
110
|
+
"fieldMapping": {
|
|
111
|
+
"mrr": "mrr",
|
|
112
|
+
"burn": "burn",
|
|
113
|
+
"runway_months": "runway_months",
|
|
114
|
+
"executedAt": "executedAt"
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
"next": { "stepId": "done" }
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
If you're inside a loop (scoring many items per run), set `source.resultsPath: "items"` so each loop iteration produces one KG row.
|
|
122
|
+
|
|
123
|
+
## Anti-patterns
|
|
124
|
+
|
|
125
|
+
**AI step with no renderer:**
|
|
126
|
+
|
|
127
|
+
```yaml
|
|
128
|
+
type: aiAction
|
|
129
|
+
responseStructure:
|
|
130
|
+
summary: string
|
|
131
|
+
kpis: { total, avgScore }
|
|
132
|
+
# ❌ No renderer — the workspace UI shows raw JSON. Nobody reads it.
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Share step without `outputSteps`:**
|
|
136
|
+
|
|
137
|
+
```yaml
|
|
138
|
+
type: share
|
|
139
|
+
shareConfig:
|
|
140
|
+
visibility: public
|
|
141
|
+
# ❌ Missing outputSteps — share URL renders nothing
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**`knowledgeSync` without a clear schema:**
|
|
145
|
+
|
|
146
|
+
If `listKey` doesn't already exist, the KG creates an implicit schema from the first write. Subsequent writes with different field shapes fail to index. For trending, pre-create the list with a typed schema.
|
|
147
|
+
|
|
148
|
+
**Reusing `fieldMapping` values with source-side renames:**
|
|
149
|
+
|
|
150
|
+
`fieldMapping` keys are source field names (from the prior step's output), values are target field names (in the KG). Flipping them silently stores the wrong data.
|
|
151
|
+
|
|
152
|
+
```yaml
|
|
153
|
+
# ✅ Correct: source → target
|
|
154
|
+
fieldMapping:
|
|
155
|
+
mrr: monthly_revenue # steps.extract.mrr → row.monthly_revenue
|
|
156
|
+
burn: monthly_expenses
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Checklist for any "report" workflow
|
|
160
|
+
|
|
161
|
+
- [ ] `responseStructure` keys match every renderer `valuePath` / `arrayPath` / `contentPath`
|
|
162
|
+
- [ ] Renderer `blocks` cover at least one KPI + one tabular view
|
|
163
|
+
- [ ] Share step exists if report is forwardable to non-workspace users
|
|
164
|
+
- [ ] If delivered via email, the email template embeds `{{steps.share.shareUrl}}`
|
|
165
|
+
- [ ] `knowledgeSync` persists to a list with a typed schema (not implicit)
|
|
166
|
+
- [ ] `executedAt` (or a similar timestamp) is stored so rows can be trended
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# CLI scaffolds
|
|
2
|
+
|
|
3
|
+
Each `*.json` file in this directory is a **preflight-clean pipeline skeleton**
|
|
4
|
+
keyed to one of the patterns in `agentic-ops/patterns/v1/` (canonical) or
|
|
5
|
+
`packages/cli/patterns/v1/` (byte-identical mirror).
|
|
6
|
+
|
|
7
|
+
Scaffolds are **pattern shapes, not domain templates.** The goal is to give
|
|
8
|
+
an agent (or human) a known-good starting point for a structural shape —
|
|
9
|
+
loop over a KG list, composed email with approval, conditional alert on
|
|
10
|
+
threshold, etc. — not to ship templates for every possible business use case.
|
|
11
|
+
|
|
12
|
+
## Contract
|
|
13
|
+
|
|
14
|
+
Every bundled scaffold:
|
|
15
|
+
|
|
16
|
+
1. Passes `agentled workflows validate --file <scaffold>` with zero errors
|
|
17
|
+
and zero warnings.
|
|
18
|
+
2. Has a clear `name` and `goal` field that describes the pattern shape.
|
|
19
|
+
3. References the matching agentic-ops pattern number(s) in its `description`.
|
|
20
|
+
4. Uses domain-agnostic placeholder names (`candidate`, `metric_a`,
|
|
21
|
+
`entity_id`) — not specific verticals.
|
|
22
|
+
|
|
23
|
+
## Catalog
|
|
24
|
+
|
|
25
|
+
| Slug | Pattern(s) | Shape |
|
|
26
|
+
|------|-----------|-------|
|
|
27
|
+
| `minimal` | — | trigger → milestone (smallest valid pipeline) |
|
|
28
|
+
| `email-polling-dedup` | 02 + 13 | schedule → fetch emails (label dedup) → loop process → add label |
|
|
29
|
+
| `lead-scoring-kg` | 04 + 09 | trigger → kg.read-list → AI scoring loop → knowledgeSync → report |
|
|
30
|
+
| `list-match-email` | 08 | trigger → kg.read-list → AI match top candidates → composed email (approval gate) → knowledgeSync |
|
|
31
|
+
| `extract-threshold-alert` | 06 + 09 | trigger → AI extract → threshold check (code) → external update → conditional Slack alert → knowledgeSync |
|
|
32
|
+
|
|
33
|
+
## Bring your own scaffolds
|
|
34
|
+
|
|
35
|
+
The bundled set is deliberately small. Workspaces, teams, or customers who
|
|
36
|
+
have recurring workflow shapes should maintain their own scaffold library
|
|
37
|
+
outside this CLI release cycle:
|
|
38
|
+
|
|
39
|
+
1. Drop JSON files in `~/.agentled/scaffolds/` or set
|
|
40
|
+
`AGENTLED_SCAFFOLDS_DIR=/path/to/your/scaffolds`.
|
|
41
|
+
2. Run `agentled workflows scaffold --list` — local scaffolds appear with
|
|
42
|
+
a `[local]` tag next to their name.
|
|
43
|
+
3. Local slugs **shadow bundled ones** with the same name, so a team can
|
|
44
|
+
override `list-match-email.json` with a locally-tuned version without
|
|
45
|
+
forking the CLI.
|
|
46
|
+
|
|
47
|
+
Any JSON in those directories must also pass `workflows validate --file`.
|
|
48
|
+
The CLI doesn't gate this at load time — but a scaffold that fails preflight
|
|
49
|
+
will waste an operator's time, which is exactly what the scaffold set is
|
|
50
|
+
meant to prevent.
|
|
51
|
+
|
|
52
|
+
## Editing the bundled set
|
|
53
|
+
|
|
54
|
+
The bundled scaffolds are meant to stay small and pattern-focused. If you
|
|
55
|
+
want to propose a new one:
|
|
56
|
+
|
|
57
|
+
- It must map to an existing agentic-ops pattern (or come with a new pattern).
|
|
58
|
+
- It must be domain-agnostic — name fields `candidate` / `metric_a` /
|
|
59
|
+
`entity_id`, not `mentor` / `mrr` / `companyId`.
|
|
60
|
+
- The `description` must state the pattern number it demonstrates.
|
|
61
|
+
- Commit both the scaffold and a preflight test verifying it passes.
|