name: synthetic-workflow-seed description: > Instantiate the LLO weekly review + program admin audit workflows — examine the labs template registry and adapt the best-fit template, or build from scratch via workflow_create (following the live authoring guide) — then wire them to the manifest's KPIs + coaching arcs. disable-model-invocation: true
Synthetic Workflow Seed
Stage 3 of ACE Phase 7 (Plan B). Creates the two demonstrative workflows
on top of the synthetic data: an operational LLO weekly review (FLW KPI
scorecard + coaching-task spawning) and a meta-level program admin audit
(week-over-week review of the LLO's process). For each, the skill examines
the labs template registry for a good fit and either adapts the closest
template (workflow_create_from_template) or builds from scratch
(workflow_create, following the live workflow_authoring_guide). It then
wires up opp-specific config + KPIs from the manifest.
The output is two workflow IDs registered in labs, one or more
synthetic OCS coaching tasks attached to underperforming FLWs, and the
pipeline schemas populated with KPI fields. The polish step
(synthetic-workflow-polish) layers per-opp visual edits on top.
Inputs
| Source | Artifact | Used for |
|---|---|---|
| Phase 7 | 7-synthetic/synthetic-narrative-plan.yaml (preferred) or synthetic-data-generate_manifest.yaml (fallback) |
kpi_config, coaching_arcs, flw_personas |
Current run's run_state.yaml |
phases.synthetic-data-and-workflows.products.synthetic.labs_opp_id (required) |
synthetic_generate_from_manifest opp scope |
| Drive | ACE/<opp>/opp.yaml |
display_name |
Products
7-synthetic/synthetic-workflow-seed.md— run summary (workflow IDs, task IDs, KPI count, polish-suitability flag)run_state.yaml.phases.synthetic-data-and-workflows.products.synthetic.workflowspopulated (read-modify-write to preserve sibling sub-keys fromsynthetic-data-generateandsynthetic-walkthrough-run):
Per-run only — no opp.yaml mirror.workflows: llo_weekly_review_id: <int> program_admin_audit_id: <int>run_state.yaml.phases.synthetic-data-and-workflows.steps.synthetic-workflow-seed.status: done- Side effects in labs:
- 1
llo_weekly_reviewworkflow with the manifest's KPIs + coaching template wired intodefinition.config - 1
program_admin_auditworkflow watching the above - 1 task per
coaching_arcs[]entry (synthetic OCS conversation embedded asdata.ocs_conversation)
- 1
Process
- Read inputs. Load the active manifest (prefer
synthetic-narrative-plan.yamlover the Stage 1 default manifest). Resolvelabs_opp_idfrom the current run'sphases.synthetic-data-and-workflows.products.synthetic.labs_opp_id. Halt if missing — this skill needs synthetic mode enabled (synthetic-data-generateshould have run first in this same run).
Build-path decision — examine templates, then adapt or author (do this before steps 2 and 7)
ACE produces two demonstrative workflows per opp — an operational LLO weekly review and a meta-level program admin audit. The two roles and the count are fixed; the build path is decided per workflow, not hardcoded to a template:
- Examine the registry. Call
mcp__connect-labs__list_templatesand read each entry (key,name,description,supports_saved_runs,multi_opp). Treat these as ideas / starting points, not a fixed menu. - Score fit against this opp: the PDD archetype, the manifest's
kpi_configshape (per-FLW aggregates? rates? facilitation-quality?), and the demonstrative intent (operational scorecard vs week-over-week oversight). A template fits when its data contract + saved-runs behavior need only config/render tailoring — NOT when you'd be working around its structure or a known defect. - Decide per workflow and record the choice + a one-line rationale
for the run summary:
- ADAPT — a registered template fits. Instantiate it with
workflow_create_from_templateand tailor it (the ADAPT branch in steps 2–4 / 7). - BUILD FROM SCRATCH — no template fits, or the closest one has a
structure/defect you'd fight. Author the workflow with
workflow_create(no template) per the SCRATCH branch.
- ADAPT — a registered template fits. Instantiate it with
Before authoring anything from scratch, fetch the current best
practices: call mcp__connect-labs__workflow_authoring_guide and follow
it (Template Anatomy, Render Code Contract, Actions API, Pipeline Schema,
Saved-runs). It is the constantly-improving source of truth served live by
labs — re-fetch every run; never author render_code from memory.
Alias-consistency guardrail (load-bearing — applies to BOTH build
paths). The pipeline's pipeline_sources[].alias, the key the
render_code reads (view.pipelines.<alias>), and the key in the
saved-run snapshot_inputs.pipelines MUST be the same string. A
mismatch (e.g. alias data while the render/snapshot read flw_kpis)
passes every create + run call yet renders blank KPIs in saved-run views
and an empty audit rollup.
- SCRATCH: you own the alias — pick one string and use it in all three places.
- ADAPT: the alias is owned by the template, not yours to
choose. For
llo_weekly_reviewthe live template alias isdata. READ the actual alias from theworkflow_create_from_templateresponse's pipelines list (orget_workflow) — do NOT assumeflw_kpisor re-author therender_codeagainst a guessed key. Any render edit you make (the Polish step'sworkflow_patch_render_code/workflow_update_render_code) and the saved-runsnapshot_inputs.pipelinesMUST use that exact template alias. Re-authoring the render against a guessed alias is the blank-KPI-in-saved-run failure: thello_weekly_reviewtemplate ships aliasdata, ACE assumedflw_kpis, and every saved-run view rendered all-zero (bednet-spot-check/20260528-0556and /20260601-0651; jjackson/ace#633).
Either way: confirm the three strings match before you ship the workflow — a blank-KPI render passes all create/run calls and only surfaces in the saved-run screenshot.
Create the LLO weekly review workflow — via the path decided above.
ADAPT branch (a template fits):
mcp__connect-labs__workflow_create_from_template( template_key: "<chosen key from list_templates, e.g. llo_weekly_review>", opportunity_id: <synthetic.labs_opp_id>, name: "<opp.yaml.display_name> — LLO Weekly Review" // optional )Capture the returned
workflow_id. The response also lists the pipelines created frompipeline_sources(the scaffold'sflw_kpi_aggregatespipeline); capture eachpipeline_idfor step 4. Then continue to steps 3–4 to wire config + pipeline.SCRATCH branch (no template fits / closest has a defect): author it with
workflow_create, following theworkflow_authoring_guideyou fetched. Build the operational FLW-scorecardrender_code, theconfig(kpi_configfrom the manifest +coaching_task_template+showSummaryCards/showFilters), and thepipeline_sources— honoring the alias-consistency guardrail above.mcp__connect-labs__workflow_create( opportunity_id: <synthetic.labs_opp_id>, name: "<opp.yaml.display_name> — LLO Weekly Review", config: { kpi_config: <manifest.kpi_config verbatim>, coaching_task_template: { subject_template: "...", ocs_persona: "..." }, showSummaryCards: true, showFilters: true, }, pipeline_sources: [ { pipeline_id: <linked/created>, alias: "<stable key>" } ], render_code: "<authored per the guide; reads view.pipelines.<same alias>>", )workflow_createreturns{workflow_id, render_code_version}. Because the SCRATCH branch already authoredconfig+render_codein this call, skip step 3 (config is set) and run step 4 only if the pipeline schema still needs itsfieldspopulated.Wire the workflow's config from the manifest.
The SEED template ships
config.kpi_config: []and a placeholdercoaching_task_template. Both get filled now via:mcp__connect-labs__workflow_update_definition( workflow_id: <from step 2>, opportunity_id: <synthetic.labs_opp_id>, expected_version: 1, // first edit since template instantiation patch: { config: { kpi_config: <manifest.kpi_config verbatim>, coaching_task_template: { subject_template: "Coaching feedback — week {week} for {flw_name}", ocs_persona: "<from manifest.coaching_arcs[0].persona, default 'supportive_coach'>", }, } } )configshallow-merges, so this preservesshowSummaryCards/showFiltersfrom the template default. OnVERSION_CONFLICT, re-fetch viaworkflow_getand retry once.Populate the pipeline schema.
The SEED template's pipeline schema ships with
fields: []because real fields depend on the opp's form schema. Build the field list fromkpi_config:# Pipeline fields use {name, path, aggregation} per # workflow_authoring_guide § Pipeline Schema — NOT {id, field_path}. # An {id, field_path} field SAVES but pipeline_preview then dies # server-side with KeyError 'name' (jjackson/ace#749). # # The manifest's KPI vocab (validated_rate / non_null_rate / # distinct_count) is GENERATOR-side only and does NOT map 1:1 onto the # pipeline's aggregation allow-list. Translate to the LIVE allow-list, # which is exactly: avg, count, count_distinct, count_unique, first, # last, list, max, min, sum. pipeline_update_schema rejects anything else # ("Unknown aggregation 'non_null_rate'"; note the live token is # count_distinct, NOT distinct_count). Typical translations: # non_null_rate on a required 0/1 field -> avg (+ a count for the denominator) # validated_rate -> avg (on the float-transformed field) # distinct_count -> count_distinct # Disclose every translation in the run summary (the # aggregation_mapping_honesty eval dimension expects it). fields = [ { "name": kpi.kpi, "path": kpi.field_path, "aggregation": translate_kpi_aggregation(kpi.aggregation), # -> pipeline allow-list above } for kpi in manifest.kpi_config ]Then call:
mcp__connect-labs__pipeline_update_schema( pipeline_id: <from step 2>, opportunity_id: <synthetic.labs_opp_id>, expected_version: 1, schema: { ...existing schema, fields: <built above> }, )On
VERSION_CONFLICT, re-fetch viapipeline_getand retry once.Shared-path guardrail (jjackson/ace#595). Do NOT emit a bare
last/first/listaggregation field on the samefield_pathas one or morecountfields that carry afilter_path/filter_valueon that path. The labs pipeline SQL generator collapses the JSONB column extraction for a shared path and the bare extraction wins, so the filteredcountfields after the first one silently compute 0 (not null — all-zero, sofields_all_nulldoes NOT catch it). This under-reports headline KPIs in the demo with no error surfaced (live on malaria-rdt 20260531-0739: alast_visit_channel(lastonform.channel_type) zeroed thepmv_samples/public_phc_samplesfiltered counts on the same path). When buildingfields: if two or more fields share afield_pathand at least one is afilter_pathcount, drop the barelast/first/liston that path (derive that value a different way, e.g. a separate filtered field or in render code). Then assert it held: after the schema save, runpipeline_preview(sample_size≥ 10) and check that nofilter_pathcountfield on a shared path is uniformly 0 across rows that have data — if one is, a shared-path collision slipped through; surface a[WARN]and remove the colliding bare aggregation. (Upstream fix tracked at jjackson/ace#595 — give each field its own filtered extraction expression.)Spawn coaching tasks.
This is the authoritative path for coaching arcs.
synthetic-data-generatedeliberately stripscoaching_arcsfrom the manifest it sends tosynthetic_generate_from_manifest(that in-generate Task path 500s — jjackson/ace#594), so the arcs are created here via the standalonetask_create_syntheticatom, which is reliable. Do NOT move arc creation back into the generate call until #594 is fixed upstream.For each entry in
manifest.coaching_arcs:mcp__connect-labs__task_create_synthetic( opportunity_id: <synthetic.labs_opp_id>, assigned_to: <coaching_arcs[].flw_id>, subject: "<coaching_task_template.subject_template formatted with the arc>", ocs_conversation: <coaching_arcs[].transcript verbatim>, status: "completed", )transcriptis the array of{role: bot|flw, text, ts}the manifest's narrative-plan authored. Capture each task's returned ID for the run summary.Per-arc try/except: if one task creation fails (e.g.
assigned_todoesn't match a real FLW the synthetic data minted), record the failure in the run summary and continue with the next arc. Other tasks proceed.Suitability check.
The seeded workflow's render_code is opp-agnostic. Decide whether
synthetic-workflow-polish(Stage 3.2) can apply surgical patches on top, or whether a full L2-mode rewrite is needed. Heuristic:scaffold_unsuitable: false(default) — the manifest'sflw_personas[].archetypematches the standard rockstar/steady/ struggling/new_hire mix, and the KPIs are per-FLW aggregates. Polish skill will doworkflow_patch_render_codeedits.scaffold_unsuitable: true— the manifest's KPIs are not per-FLW aggregates (e.g., a focus-group archetype with facilitation-quality KPIs that aren't worker-rollable), or the archetype mix is exotic. Polish skill will rewrite the render code from scratch (workflow_update_render_code).
For the canonical atomic-visit archetype with per-FLW KPIs, this is always
false. Record the flag in step 8.Create the program admin audit workflow — via the path decided above.
ADAPT branch (a template fits — usually
program_admin_report, the live registry key for the cross-opp SOP-compliance rollup; confirm the exact key fromlist_templatesrather than assuming):mcp__connect-labs__workflow_create_from_template( template_key: "<chosen audit key from list_templates>", opportunity_id: <synthetic.labs_opp_id>, name: "<opp.yaml.display_name> — Program Admin Audit" )SCRATCH branch (no audit template fits): author with
workflow_createper theworkflow_authoring_guide— a meta-level workflow whose render reads the watched LLO weekly review's saved-run snapshots (it has no pipeline of its own).Either way, wire the watched workflow:
mcp__connect-labs__workflow_update_definition( workflow_id: <from step 7>, opportunity_id: <synthetic.labs_opp_id>, expected_version: 1, patch: { config: { watched_workflow_id: <llo_weekly_review_id from step 2> } } )The audit workflow renders a week-over-week compliance dashboard from its snapshot's
watched_summary. Note: thisconfigwiring is definition-level metadata only — thebuild_snapshothook does NOT read watched sources from the config. The functional requirement is the per-runinitial_stateset in step 8b (and the hook's key isworkflow_definition_id, whereas the config field iswatched_workflow_id/watched_sources[].workflow_id— don't assume they're the same name).Saved-runs progression — Week 1 + Week 2 snapshots.
Plan B's Task 3.1 calls for a Week 1 + Week 2 saved-runs progression to make the demo show week-over-week deltas. The connect-labs MCP shipped
workflow_create_run+ a fixedworkflow_save_snapshot(withopportunity_idscope) to support this loop programmatically.Derive Week 1 and Week 2 windows from the manifest's
timeline:start = manifest.timeline.start_date # ISO date week_1_end = start + 7 days week_2_start = start + 7 days week_2_end = start + 14 daysThen for each week, do create-then-snapshot against the LLO weekly review workflow. The audit's rollup is NOT free — despite the old "it just reads the LLO review's snapshots" assumption, the audit's
watched_summaryis built by a server-side hook that fires only when an audit run is snapshotted, and reads its inputs from that run's own state. Step 8b below creates it. (Root-caused live 2026-05-31 against workflow 3448 — jjackson/ace#596.)# Week 1 r1 = mcp__connect-labs__workflow_create_run( definition_id: <llo_weekly_review_id>, opportunity_id: <synthetic.labs_opp_id>, period_start: <start>, period_end: <week_1_end>, ) mcp__connect-labs__workflow_save_snapshot( run_id: r1.run_id, opportunity_id: <synthetic.labs_opp_id>, # required since labs PR #168 snapshot_name: "Week 1", captured_at: "<week_1_end>T23:59:59Z", ) # Week 2 — same shape with bumped datesCapture both run IDs and snapshot timestamps for the run summary.
⚠️ The two LLO-review snapshots currently render IDENTICAL (jjackson/ace#764). The
flw_kpisrollup aggregates all of the opportunity's visits and is NOT period-filtered by the run'speriod_start/period_end(the labs pipeline groups bycompleted_at, andsave_run_snapshotfreezes a non-period-scoped aggregate). So Week 1 and Week 2 snapshots show the same numbers. This is a labs-side root cause (not local; the pipeline must threadperiod_start/period_endintoflw_kpisas avisit_datefilter). Until it lands, do NOT market the two snapshots as week-distinct in the walkthrough or polish narrative. Either (a) present them as "the same review, run on a recurring cadence" without asserting week-over-week deltas, or (b) point the "recurring rhythm" scene at a page that IS genuinely distinct per period (the program_admin_audit window aggregate) rather than the identical LLO-review weekly snapshots.synthetic-workflow-polishmust keep any week-referencing narrative conditional on real per-period difference — see its SKILL.md note.Per-week try/except: if Week 1 succeeds but Week 2's create or snapshot fails (e.g. transient labs error), surface the partial completion in the run summary and continue. Operator can re-run
/ace:step synthetic-workflow-seedto retry — but note that re-runs create NEW workflows + duplicate runs (no idempotency on labs side yet); operator should useworkflow_deleteto retire stale ones before retrying.Why Week 1 + Week 2 specifically: the program admin audit workflow renders week-over-week LLO-process compliance from multiple snapshots. Two weeks is the minimum for a "trend"; more weeks add noise to the demo without meaningful narrative gain. Operators wanting a longer run can edit the manifest's timeline to N weeks, then call
workflow_create_run+workflow_save_snapshotN times via direct MCP calls.
8b. Snapshot the program admin audit run (REQUIRED — without this the audit renders "0 opportunities watched" + an empty WINDOW AGGREGATE).
**First probe whether the deployed template still has a build_snapshot
hook** — `workflow_get(<program_admin_audit_id>)` and read
`has_build_snapshot_hook`. The two template generations behave
differently, and the currently-deployed one ships WITHOUT the hook
(jjackson/ace#750):
- **`has_build_snapshot_hook: true`** — the `program_admin_report`
template's `build_snapshot` hook computes `watched_summary` server-side
when an *audit* run is snapshotted, reading its inputs from the audit
run's **own `state`** (`run.data.state`) — NOT from the definition
config, and NOT "for free" from the LLO review snapshots. Author the
window + watched sources into `initial_state` (below) and the hook
builds the rest.
- **`has_build_snapshot_hook: false`** (the currently-deployed template) —
NO hook runs, so `watched_summary` is never computed and the audit
renders "0 opportunities watched." `workflow_save_snapshot` freezes
`initial_state` **verbatim** (`save_run_snapshot` is a dumb freeze), so
you MUST author the fully-computed `watched_summary` directly into
`initial_state` alongside the window — see the `watched_summary` block
below. This is the live workaround used on bednet-spot-check runs until
the labs template regains the hook (labs-side root cause: jjackson/ace#750).
Either way, after the LLO-review weeks are seeded, create + snapshot
exactly **one** audit run whose `initial_state` carries the window +
watched sources (and, when the hook is absent, the computed
`watched_summary`) in the exact shape the render reads (verified live
2026-05-31 / 2026-06-12, jjackson/ace#596 + #750):
```
# Mondays covered by the window, one ISO date each (drives the grid columns)
expected_weeks = [ <Monday ISO> for each week in the window ]
audit_run = mcp__connect-labs__workflow_create_run(
definition_id: <program_admin_audit_id>,
opportunity_id: <synthetic.labs_opp_id>,
period_start: <window_start date>,
period_end: <window_end date>,
initial_state: {
# Window the hook FILTERS watched runs by. Must be END-OF-DAY
# inclusive: the LLO-review snapshots you just saved have
# completed_at = NOW (wall-clock), and a date-only window_end
# parses to MIDNIGHT, which excludes same-day snapshots — the
# exact "empty aggregate" trap. Use a T23:59:59Z upper bound that
# is >= the latest snapshot's completed_at.
window_start: "<window_start>T00:00:00Z",
window_end: "<today>T23:59:59Z",
# Calendar strings the render shows verbatim (keep these date-only).
display_window_start: "<window_start>",
display_window_end: "<window_end>",
expected_weeks: expected_weeks,
# One entry per watched workflow. The hook does
# source["workflow_definition_id"] — the key MUST be
# workflow_definition_id, NOT workflow_id (the config field name).
watched_sources: [
{ name: "<llo review display name>",
workflow_definition_id: <llo_weekly_review_id>,
opportunity_id: <synthetic.labs_opp_id> }
],
# REQUIRED when has_build_snapshot_hook == false (#750): author the
# COMPUTED rollup directly — the render reads state.watched_summary and
# (with no hook) nothing else builds it. One entry per watched
# opportunity; runs[] is one row per seeded LLO-review snapshot
# (Week 1, Week 2, ...). Compute flw_count / missed_week_idxs from the
# same data you seeded. Read the EXACT field names from the live render
# code (workflow_get -> render_code) before authoring — they must match
# what the template renders. OMIT this block when
# has_build_snapshot_hook == true (the hook computes it).
# watched_summary: [
# { opportunity_id: <synthetic.labs_opp_id>,
# workflow_definition_id: <llo_weekly_review_id>,
# label: "<llo review display name>",
# network_manager: "<nm name or null>",
# flw_count: <int>,
# missed_week_idxs: [<int>, ...],
# runs: [ { period_start, period_end, snapshot_name, captured_at, run_id }, ... ] },
# ]
},
)
mcp__connect-labs__workflow_save_snapshot(
run_id: audit_run.run_id,
opportunity_id: <synthetic.labs_opp_id>,
snapshot_name: "Audit rollup",
captured_at: "<now ISO>",
)
```
**Verify it populated** (don't trust the save's 200): GET
`${LABS_BASE_URL}/labs/workflow/api/run/<audit_run_id>/snapshot/?opportunity_id=<opp>`
and assert `snapshot.state.watched_summary` is non-empty with `runs` per
source. Diagnostics if it isn't:
- `snapshot.error == "missing_window"` → `window_start`/`window_end`
missing from `initial_state`.
- sources present but every `runs: []` → `window_end` was date-only
(midnight) and excluded the same-day snapshots, OR a `watched_sources`
entry used `workflow_id` instead of `workflow_definition_id`.
Record the `audit_run_id` for the run summary.
**Known residuals (NOT fixed by this step — labs-side; see
jjackson/ace#596 + the connect-labs cadence-backdating issue):**
- The cadence **grid** can't show a real week-over-week trend yet:
`workflow_save_snapshot` has no `completed_at` override, so every
synthetic watched snapshot lands at wall-clock-now (the current week),
and the rollup/render group by `completed_at`, not run `period`. The
window AGGREGATE populates correctly; the per-week columns will show
the runs only in the current week.
- `flw_rows` (per-FLW flags/audits/tasks drill-down) are empty unless the
governance artifacts are seeded for the watched runs via
`program_admin_demo_seed`.
Write the run summary to
7-synthetic/synthetic-workflow-seed.mdviadrive_create_file(find-or-update — re-runs overwrite). Body:- Workflow IDs:
llo_weekly_review_id,program_admin_audit_id - Public labs URLs:
${LABS_BASE_URL}/labs/workflow/<id>/ - Render deep-links (with
run_id— this is what downstream skills screenshot/link): the bare/labs/workflow/<id>/?opportunity_id=<opp>URL renders the run picker, not the polished per-FLW dashboard — the completed render only mounts when a savedrun_idis in the URL. So emit, for each workflow that saved a run:- LLO Weekly Review:
${LABS_BASE_URL}/labs/workflow/<llo_weekly_review_id>/run/?run_id=<latest run_id = Week 2, else Week 1>&opportunity_id=<labs_opp_id> - Program Admin Audit:
${LABS_BASE_URL}/labs/workflow/<program_admin_audit_id>/run/?run_id=<audit_run_id>&opportunity_id=<labs_opp_id>synthetic-summary,synthetic-workflow-polish-eval(step 6 capture), andsynthetic-walkthrough-specall consume these. Recipe verified live against labs prod (jjackson/ace#769; jjackson/connect-labs#541).
- LLO Weekly Review:
- KPI count, coaching-task count, scaffold-suitability flag
- Saved-runs:
Week 1 run_id=<int>,Week 2 run_id=<int>,<n>/2 snapshots saved(when both land cleanly), or partial-week failures with the labs error verbatim - Any per-arc failures from step 5
- Workflow IDs:
Update
phases.synthetic-data-and-workflows.products.synthetic.workflowsin the current run'srun_state.yaml. Read-modify-write to preserve sibling sub-keys (synthetic-data-generateownsenabled/current_*/labs_opp_id/fixture_record_counts;synthetic-walkthrough-runownswalkthroughs[]):drive_read_fileon the current run'srun_state.yaml; extract the existingproducts.syntheticblock.- Merge in this skill's contribution:
synthetic: # preserved siblings: enabled: true current_folder_id: ... labs_opp_id: ... # this skill's keys: workflows: llo_weekly_review_id: <int> program_admin_audit_id: <int> update_yaml_filewithmerge: 'deep'on thephases.synthetic-data-and-workflows.products.synthetic.workflowspayload (deeppreserves sibling sub-keys + the phase'sstatus/steps;two-levelwould replace the whole phase block, #572/#587).
No write to
opp.yaml.synthetic— synthetic state is per-run only.Update
run_state.yamlvia the read-merge-write pattern (NOTupdate_yaml_file— same caveat assynthetic-data-generatestep 6 on the phases-block clobber risk):phases: synthetic-data-and-workflows: steps: synthetic-workflow-seed: status: done workflow_ids: llo_weekly_review: <int> program_admin_audit: <int> kpi_count: <int> coaching_task_count: <int> scaffold_unsuitable: <bool> saved_runs_status: deferred-to-labs-pr artifacts: summary: <Drive ID>
MCP Tools Used
mcp__connect-labs__list_templates(build-path decision — examine the registry)mcp__connect-labs__workflow_authoring_guide(fetch before any from-scratch authoring)mcp__connect-labs__workflow_create_from_template(ADAPT branch)mcp__connect-labs__workflow_create(SCRATCH branch — author without a template)mcp__connect-labs__workflow_update_definitionmcp__connect-labs__workflow_update_render_code(SCRATCH branch — if render is authored after create)mcp__connect-labs__workflow_get(onVERSION_CONFLICTretry)mcp__connect-labs__pipeline_update_schemamcp__connect-labs__pipeline_get(onVERSION_CONFLICTretry)mcp__connect-labs__task_create_syntheticmcp__connect-labs__workflow_create_run(step 8 — Week 1 + Week 2 saved-runs)mcp__connect-labs__workflow_save_snapshot(step 8 — completes each week)mcp__plugin_ace_ace-gdrive__drive_read_filemcp__plugin_ace_ace-gdrive__drive_create_file(find-or-update)mcp__plugin_ace_ace-gdrive__drive_update_file(run_state read-merge-write)mcp__plugin_ace_ace-gdrive__update_yaml_file— writesphases.synthetic-data-and-workflows.products.synthetic.workflowstorun_state.yaml(merge: 'deep'— preserves sibling sub-keys + the phase's status/steps; #572/#587)
Mode Behavior
- Default: create both workflows, wire config, spawn coaching tasks, skip saved-runs, write summary. The ~30s end-to-end produces a labs state ready for the polish step.
--skip-coaching-tasks: create workflows + wire config but skip step 5. Useful when iterating on the workflow definition without re-spawning task records.--llo-only/--audit-only: create only one of the two workflows. Mostly for debugging — the demo story needs both.
Dry-Run Behavior
--dry-run writes the run summary describing what would be created
(workflow IDs marked <TBD>) and skips all labs MCP mutations. State
tracks as dry-run-success.
Failure Modes
| Failure | Detection | Recovery |
|---|---|---|
synthetic.labs_opp_id missing in opp.yaml |
step 1 halt | Run synthetic-data-generate first to enable synthetic mode for this opp. |
workflow_create_from_template returns 4xx |
step 2 halt | Surface the labs error verbatim. Common cases: caller not a member of the opp's organization (check labs_context); template_key typo. |
kpi_config empty in manifest |
step 3 warn | The workflow gets created with no KPIs and the per-FLW table renders empty. Edit the manifest to add at least one KPI then re-run. |
| One coaching arc fails | step 5 partial | Other arcs proceed; failure recorded in run summary. Re-run with --skip-coaching-tasks then create individually via task_create_synthetic if desired. |
workflow_create_run or workflow_save_snapshot returns transport error |
step 8 partial | Capture the labs error in the run summary; re-run /ace:step synthetic-workflow-seed after the transient resolves. Idempotency caveat: re-runs create NEW workflow definitions; use workflow_delete to retire stale ones first OR open the just-failed workflow in labs UI and finish the snapshot manually. |
workflow_save_snapshot returns INVALID_SCHEMA cross-check error |
step 8 halt | The run's opportunity_id doesn't match the param. Should never fire if step 8's loop is built correctly (uses the same synthetic.labs_opp_id for both create and snapshot). If it does, the run record was created against a different opp than the skill thinks — investigate via workflow_get. |
| Re-run on existing workflows | step 2 idempotency | workflow_create_from_template always creates a NEW workflow (no find-or-create on labs side). Re-runs append to opp.yaml; old workflow_ids are orphaned in labs and need manual cleanup. Use workflow_delete directly if you need to retire a stale instance. |
Saved-run views render blank KPIs (— tiles) — either build path |
post-build smoke (walkthrough screenshot) shows — for KPI tiles |
The pipeline_sources[].alias, the render's view.pipelines.<alias> read, and snapshot_inputs.pipelines disagree. SCRATCH: pick one alias for all three. ADAPT: the alias is the template's (llo_weekly_review → data) — read it from the create-from-template response, don't assume flw_kpis. See the alias-consistency guardrail before step 2 (jjackson/ace#633). |
Related skills
synthetic-data-generate— produces the synthetic data this skill's workflows render against.synthetic-narrative-plan— produces the manifest withkpi_configandcoaching_arcsthis skill consumes.synthetic-workflow-polish— Stage 3.2 sibling that applies per-opp visual edits on top of the seeded workflows. Run after this skill.synthetic-summary— links to the workflow URLs once they exist.
Removal criteria
This skill is permanent. Saved-runs creation (step 8) was deferred to a
labs PR at first ship; that gap closed in 0.13.64 once labs shipped
workflow_create_run + the workflow_save_snapshot opp-scope fix
(connect-labs PR #168). No remaining [WAITING ON LABS] items.
Change Log
| Date | Change | Author |
|---|---|---|
| 2026-05-06 | Initial Stage 3a skill — workflow seeding via SEED templates + config wiring + coaching tasks. Saved-runs deferred to labs PR. | ACE team (Plan B Stage 3) |
| 2026-05-07 | Step 8 wires the full Week 1 + Week 2 saved-runs loop via workflow_create_run + workflow_save_snapshot (now scoped to opportunity_id per labs PR #168). Removes the last [WAITING ON LABS] deferral in Phase 7. Live-smoked against turmeric (workflow 2847, run_ids 2859 + 2860, both snapshots saved cleanly). |
ACE team (Plan B Stage 3b) |
| 2026-05-28 | Adapt-or-author build path. Added the Build-path decision step (examine list_templates → ADAPT closest template or BUILD FROM SCRATCH via workflow_create), ADAPT/SCRATCH branches on steps 2 + 7, the fetch-workflow_authoring_guide-before-scratch rule, and the alias-consistency guardrail (pipeline alias == render key == snapshot_inputs key — the blank-KPI failure from bednet-spot-check/20260528-0556). Unblocked by connect-labs#300 (workflow_create + workflow_authoring_guide). Also resolves the stale program_admin_audit template_key — the live key is program_admin_report, and the examine step now reads live keys. |
ACE team |
| 2026-06-13 | Step 9 summary now emits Render deep-links (/labs/workflow/<id>/run/?run_id=<run_id>&opportunity_id=<opp>) for each saved run, so downstream consumers (synthetic-summary, synthetic-workflow-polish-eval capture, synthetic-walkthrough-spec) link the polished dashboard, not the run picker. The bare /workflow/<id>/?opportunity_id= URL renders the picker — the completed render needs a run_id. See docs/learnings/2026-06-13-labs-workflow-run-deeplink.md (jjackson/ace#769). |
ACE team |
| 2026-06-15 | Render deep-link URL corrected (#788). The emitted form was …/run/<run_id>/?opportunity_id= — run_id as a PATH segment, which matches no labs route (workflow/urls.py has only <def>/run/ reading ?run_id= and run/<run_id>/), so headless capture got Resolver404. Fixed to the query-param form …/run/?run_id=<run_id>&opportunity_id= across this skill + synthetic-summary / synthetic-workflow-polish-eval / synthetic-walkthrough-spec + the learning doc. Confirmed from labs source: WorkflowRunView reads run_id via request.GET, and validate_context_access passes opportunity_id through even on empty organization_data (so org_data was never the blocker). |
ACE team |