feat(conductor): workflow-template library — bundled catalog, slot mapping, guided instantiation (#429)#479
Merged
Merged
Conversation
…itution (#429) TemplateManifest/TemplateSlots/TemplateSlotMapping types plus pure, I/O-free helpers: extractSlotRefs (structural walk of the five ref fields), missingSlotMappings, applyTemplateSlots (deep-clone, field-targeted slot:<kind>:<key> substitution, {{ctx.*}} interpolation untouched) and checkTemplateManifest (metadata, structural validate, bidirectional slot coverage, duplicate keys, malformed refs). Foundation for the file-based template catalog and /templates routes.
…ild plumbing (#429) Four curated TemplateManifest JSONs (expense-approval, notify-and-escalate, weekly-report, onboarding-checklist) in middleware/src/conductor/templates/, a dirname-relative loadTemplateCatalog() that skips invalid assets with a log line instead of failing boot, a CI gate test proving every bundled template is valid and instantiable end-to-end (synthetic mapping + validate() with KnownRefs + cron validity), and build plumbing so both plain npm run build (copy-build-assets.mjs) and the Docker image carry the JSON assets. File-based catalog, no DB migration.
#429) Expose the bundled template catalog on the operator conductor API and add the two instantiation paths, both validating with live KnownRefs (agent slugs, action ids, role keys, event catalog) — deliberately stricter than POST / so a template instance is runnable, not merely well-formed. Instantiate publishes through the ordinary createOrPublish path with the atomic cron-schedule reconcile and 409s on slug collision instead of upserting. Resolve is the ephemeral seam for #330 and the UI's open-in- designer flow. Routes registered before the /:slug catch-all; stub-dep route tests cover every status code, the createOrPublish call shape, and a route-order regression. API documented in the middleware handoff doc.
Card grid above the workflows list rendering the bundled template catalog from GET /templates: name, useCase tag, description, pluralized slot-mapping summary and a text/edge schedule badge for cron templates. API client mirrors the TemplateManifest wire shape locally and ships the resolve/instantiate fetchers for the follow-up mapping-form unit. i18n en+de; Vitest component tests.
Completes the guided instantiation flow the f1 gallery opened: one upfront mapping form per template instead of per-node walking, because declared slots make a single form sufficient and the server's completeness check already names every gap. Create publishes via POST /templates/:id/ instantiate; Open in designer resolves ephemerally and reuses the existing chat-to-canvas hydration so the canvas needs no changes. The enable toggle defaults to off with a persistent schedule notice for cron templates — schedule transparency is a must-have, never hover-hidden. Channel slots prefill 'teams' so the mapping state never diverges from what the shared ChannelSelect displays.
Review fixups: - The Dockerfile COPY for the template JSON assets was redundant (the builder stage's npm run build already mirrors them into dist via copy-build-assets.mjs, and the runtime stage copies dist wholesale) and the review gate forbids touching deployment config — reverted to base. - "Open in designer" now hands the form's instance slug/name to the canvas alongside the resolved graph (CanvasGraphRequest gains optional slug/name), so Save is armed on a fresh page and never republishes the template graph over a previously loaded workflow's slug. Chat-pane draft pushes omit the fields and leave the canvas identity untouched.
…r save path (#429) The 'Open in designer' handoff passed only graph/slug/name, and ConductorCanvas.handleSave hardcoded publishConductorWorkflow enable: true — so a cron template left on the default-off toggle was created enabled on Save and its schedule started without the form's schedule notice applying. onOpenInDesigner and CanvasGraphRequest now carry the form's enable flag; the canvas publishes with it and resets to the historical enabled-on-save behaviour for chat drafts and loaded workflows (the store only applies enable on first create anyway). Regression tests: new ConductorCanvas.test.tsx (stubbed @xyflow/react) plus extended form handoff tests.
…an for bundled templates (#429) Template name, description, useCase, and slot labels/help texts rendered raw English in the German UI. Templates are data (v2 distributes them outside the repo), so localization travels in the manifest: these fields now accept a plain string or an { en, de?, ... } record with en required as the universal fallback. - conductor-core: LocalizedText/LocalizedTextMap types, resolveLocalizedText, checkTemplateManifest validates the localized shape (new template_invalid_localized_text code); missing-slot envelopes keep flat English labels (clients localize by kind+key from the manifest they hold) - bundled manifests: proper German translations for all operator-facing texts; the catalog CI gate now asserts bundled en/de parity - routes: GET /templates keeps returning full, unresolved manifests (#330); the instantiate route resolves its manifest-borne name/description fallbacks to the en base before persisting - web-ui: gallery + slot-mapping form resolve the active locale via next-intl useLocale() with en fallback (resolveConductorText); component tests cover German rendering under de and the en fallback
…ate-only publish (#429) Two concurrent POST /templates/:id/instantiate with the same not-yet-existing slug both passed the getBySlug pre-check; the loser fell into createOrPublish's ON CONFLICT DO UPDATE upsert and silently published a second version over the just-created workflow, answering 201 — violating the route's create-new contract. - ConductorWorkflowStore.createOrPublish gains expectNew: the conflict clause flips to INSERT ... ON CONFLICT (slug) DO NOTHING; zero returned rows aborts the publish transaction with the new WorkflowSlugExistsError - the instantiate route drops the racy pre-check entirely and maps the error to the existing 409 conductor.slug_exists envelope — of two racing instantiates exactly one INSERT wins - POST / and the canvas save path keep their idempotent upsert untouched (regression-tested: no expectNew leaks into the ordinary publish) - store-level tests (fake pool, SQL-shape scripted) prove the create-only conflict semantics incl. rollback; route tests prove the 409 mapping
This was referenced Jul 10, 2026
Merged
* feat(conductor-core): template manifest v2 — version, text slots, inference, strict gate (#478) Additive extension of the #429 template contract, pure functions only: TemplateManifest.version (absent = 1, read via templateManifestVersion), declared text slots substituted as slot:text:<key> tokens in the two designated prose fields (step.prompt, human.message; disjoint from {{...}} run-context interpolation, structural walk, never string-replace over JSON), TemplateSlotMapping.text, kind:'text' missing-slot entries, checkTemplateManifest text-slot coverage codes plus a { strict } mode that rejects concrete refs in distributed (plugin/hub) manifests, and inferTemplateManifest reversing the extractSlotRefs walk for the save-as-template path (slugified de-duplicated keys, pre-existing placeholders preserved, round-trip tested). Text machinery split into textSlots.ts and v2 tests into templateV2.test.ts to keep every file under the 500-line budget; the v1 test file stays byte-identical as the contract pin for #330 consumers. * feat(conductor): DB template store, composite catalog, CRUD + versioning routes (#478) User-authored workflow templates move into the conductor DB: migration 0006_templates.sql (conductor chain) adds conductor_templates, conductor_template_versions (immutable JSONB manifests), conductor_template_instantiations (append-only anonymous telemetry) and template provenance columns on conductor_workflows. A composite catalog merges bundled files, DB user templates and a plugin registration seam behind one viewer-scoped interface whose visibility rule makes 'pending' templates reachable by every operator — every operator on the single-tier operator API is a potential reviewer, so the review gate cannot collapse into author-only self-approval. New CRUD + versioning routes (split into templateRoutes.ts for file size) keep the GET /templates wire shape additive over v1 so the #330 consumer contract holds; resolve/instantiate accept an explicit version and instantiate stamps provenance in the same transaction as the publish. Conductor migrations are now also mirrored into dist by copy-build-assets.mjs, which previously relied on the Dockerfile COPY alone. * feat(conductor): save-as-template inference, review gate, plugin-borne templates, update hint (#478) Unit B3 of the templates-v2 plan: - POST /:slug/save-as-template returns an inferTemplateManifest DRAFT (one declared slot per distinct concrete ref) from the workflow's active published version; nothing persisted, UI publishes via POST/PUT /templates. Default id derives from the slug with a -template suffix on collision. - Review state machine private -> pending -> shared: submit (author-only, 409 template_status_conflict otherwise), approve/reject (ANY operator via the viewer-scoped catalog get - reachable because pending is visible install-wide; reviewed_by recorded; self-approval permitted and auditable). - Workflow list/detail additively report template?: { id, version, latestVersion, updateAvailable } from the template_id/ template_version provenance columns (viewer-scoped, no existence leak); workflowStore now selects those columns. - Plugin-borne templates (trust boundary in docs/security-architecture section 4): permissions.templates declares package-relative JSON manifests; fail-closed install gate in src/plugins/pluginTemplates.ts (path confinement after symlink unwrapping, plugin:<id>:<name> namespacing, checkTemplateManifest strict mode, isValidCron); any violation fails the install with install.template_invalid. Accepted manifests register as read-only source 'plugin' catalog entries, unregister on uninstall, and re-register at boot (fail-open sweep). Templates are data, never code - no runtime template API. Tests: test/pluginTemplates.test.ts (new) + extended test/conductorTemplateRoutes.test.ts (48 route cases incl. non-author approve, inference round-trip, update hint, plugin source read-only). * feat(conductor): builder-chat template awareness (#478) The conversational builder can now see the workflow-template catalog and suggest templates in chat: its system prompt carries a viewer-scoped catalog digest (capped at 30 entries) and the reply protocol accepts a templateProposals block that POST /builder/turn returns additively (the key is absent without proposals, keeping the v1 wire shape byte-identical). Proposals are vetted server-side inside the agent seam so the LLM cannot surface templates the viewer must not see: unknown/invisible ids dropped against the composite catalog, max 3 deduped proposals, version taken from the catalog rather than the model, and prefill guesses kept only for declared slot keys whose ref values resolve against the live KnownRefs sets (shared with the resolve/instantiate routes via a hoisted templateKnownRefs so the two gates cannot drift). Chat proposes and prefills only - instantiation stays on the deliberate form flow. * feat(web-ui): save-as-template dialog with owner-aware version publish (#478) Authoring UX in the Conductor admin UI over the backend's slot-inference draft (POST /:slug/save-as-template): edit metadata and inferred ref-slot labels (en/de LocalizedText, en required), declare text slots with the paste-able slot:text:<key> token, and publish. The primary action is owner-aware: fresh id posts a new template, an id owned by the viewer switches to 'Publish as v{latestVersion+1}' via PUT, a foreign/bundled id shows an inline error, and a 409 race on POST re-checks ownership and flips the dialog into the PUT state instead of dead-ending. This makes publisher maintenance (v>1) reachable from the UI, not just the raw API. * feat(web-ui): gallery v2 facets, pending-review queue, search, manage actions (#478) TemplateGallery renders the composite catalog with provenance facets (All / Bundled / My templates / Shared / Plugins / Pending review), client-side text search over the locale-resolved name/description/useCase, and secondary use-case chips. The Pending review facet is the reviewer queue: every pending user template is listed for EVERY operator (the install-wide pending-visibility rule makes the review gate reachable by non-author reviewers), with the submitter shown and Approve/Reject actions directly on the card, plus a waiting-count badge on the facet label. Cards gain a provenance/status badge (text + edge color only, per Lume), a v{n} tag, an instantiation count, and author manage actions on own user templates: Submit for review (private only) and Delete behind an inline confirm. Mutations refetch the catalog via the page (onCatalogChanged -> reload); errors surface inline as text with the server's message. API client gains deleteConductorTemplate, submitConductorTemplate, approveConductorTemplate, rejectConductorTemplate. Vitest covers facet filtering (viewer-owned only under My templates), the non-author pending queue with a working Approve (and the card surfacing under Shared after refresh), locale-fallback search, author-only manage actions, the delete confirm, and absence of review actions on non-pending cards. * feat(web-ui): instantiate form v2 — text slots, graph preview, version pin, update hint (#478) Text-slot inputs (required-fill unless a default exists; kind:'text' server errors mapped inline; mapping carries the additive text record), a collapsed- by-default read-only graph preview rendered from the manifest (TemplatePreview on @xyflow/react — the designer's actual stack, not the plan's Cytoscape), version display + explicit-version resolve/instantiate, an initialMapping prefill seam for the chat proposals (F4), and the opt-in update path: workflows carrying template.updateAvailable render 'Template updated (v{n} -> v{m})' with a re-instantiate action opening the form pinned to the latest version (TemplateUpdateHint; copy-not-reference — the old workflow stays untouched). API client: mapping.text + version on resolve/instantiate, fetchConductorTemplateVersions, template hint on the workflow wire type. * feat(web-ui): chat template proposal cards in the builder pane (#478) Render the builder turn's templateProposals (B4) as up to 3 compact cards under the assistant reply: locale-resolved template name, v{n} tag, one-line reason, and a slot-coverage line counted against declared slots only. The single 'Use template' action hands off to the instantiate form through the page's existing state plumbing (prefill analog of the chat->canvas setChatGraphRequest hand-off): the form opens pinned to the proposed version with the proposal's prefill as initialMapping, re-keyed per hand-off. Chat never auto-instantiates. Unknown template ids degrade to plain text; turns without proposals render exactly as before (API client type is additive). i18n en+de; 5 vitest cases (cards, hand-off payload, no-proposal regression, >3 cap, degrade). * refactor(web-ui): step-kind Lume tokens + 500-line splits (#478) Review fixups for the templates-v2 branch: - Lume gate: the step-kind palette (agent/action/human + badge text) was hardcoded hex twice (ConductorCanvas, TemplatePreview). It now lives as --step-kind-* tokens in theme.css, consumed by both components through the shared stepKindColors.ts map — no hex in component code, no keep-in-sync comment. - 500-line rule: SaveAsTemplateDialog (527) extracted its slot editor sections into SaveAsTemplateSlotEditors.tsx (388/197); conductor/page.tsx (604) extracted the Roles and emit-event sections into ConductorRolesSection.tsx and ConductorEmitSection.tsx (464). Behavior-preserving: role/emit errors and refetches still route through the page, and the double-fire guard is shared via a guard callback (prop-object mutation trips react-hooks/immutability). * fix(conductor): harden manifest gate, place text-slot tokens (#478) Codex review round 2 on the templates-v2 branch found three blockers: - POST /conductors/templates with {} (or any manifest whose slots field / kind lists / entries have the wrong shape) crashed checkTemplateManifest and surfaced as a 500. The gate now shape-checks slots before walking and reports malformed input as ordinary validation errors, so the route answers 400 conductor.template_invalid. The localized-text helpers moved to localizedText.ts to keep template.ts within the 500-line budget; the @omadia/conductor-core export surface is unchanged. - Save-as-template text slots were undeliverable: the dialog declared them but never put a slot:text:<key> token into the graph, so the backend rejected every such manifest as template_text_slot_unused. The dialog now edits the graph's designated step texts (step.prompt, human.message) with per-field token insert buttons and blocks publish until every declared slot's token is placed; the edited texts travel in the manifest graph. - Committed trailing whitespace in the conductor template test files broke git diff --check hygiene; stripped. * fix(web-ui): gate save-as-template ownership check while auth resolves (#478) An owned template id no longer dead-ends on 'id taken' when the dialog opens before getAuthMe settles. The page now tracks viewerPending, and the dialog derives a fourth publishMode 'pending' from live props: a user-sourced collision holds a gated 'Checking ownership' state (Lume busy-dots, submit disabled) and flips to 'Publish as v{n+1}' or the id-taken error once the viewer is known. Bundled/plugin collisions stay terminal, and the 409-race re-check is now pending-aware too.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A curated, file-based workflow template catalog for Conductor with a declared-slot parameterization layer and a guided instantiation path: pick template → map slots → validated publish → ordinary workflow.
conductor-core: template contract (
middleware/packages/conductor-core)TemplateManifesttype: a completeWorkflowGraphwhose external references (agent slugs, action ids, role keys, event ids, delivery channels) areslot:<kind>:<key>placeholders, plus catalog metadata (id,name,description,useCase,defaultSlug) and aslotsdeclaration enumerating every placeholder with a human-readable label.substituteSlots()— substitution is deterministic and covered by unit tests; no shared client code needed.Middleware: bundled catalog + routes
middleware/src/conductor/templates/: expense approval with escalation, notify-and-escalate, weekly scheduled report, onboarding checklist. File-based, loaded at wire time — zero migrations.conductorGraphSchemaand the structural validator in tests, so the catalog cannot drift from the engine.GET /templates— full manifests incl. graph + slot declarations (machine-readable, settles the omadia Facilitator — agent that moderates group chats to a defined outcome (+ Conductor JIT/ephemeral workflows & Core group primitives) #330 seam),POST /templates/:id/resolve— server-side substitution + validation with liveKnownRefs, returns the resolved graph without persisting (ephemeral instantiation),POST /templates/:id/instantiate— same resolution path, then publishes through the existingConductorWorkflowStore.createOrPublish()with the atomic cron-schedule reconcile;enabledefaults tofalse. The result is an ordinary versioned workflow — copy-not-reference, fully editable, no link back to the template.template_slot_mapping_incomplete(per-slot detail),slug_exists(409),invalid_graph(validator error list).web-ui: gallery + guided instantiation on
/conductorChannelSelect. Server error envelopes map back to inline field errors. Lume-conform: state colors as text/edge only, Button busy pattern (no spinners), all strings i18n'd (en + de).instantiate) and Open in designer (resolve→ the graph hydratesConductorCanvasvia the existing chat→canvasloadGraphRequestmechanism, extended to carry the instance slug/name — publish then goes through the canvas's normal save flow).Review fix: designer handoff honors the enable=OFF default
The "Open in designer" handoff previously dropped the form's default-off enable toggle: the canvas save path hardcoded
enable: true, so a cron template left disabled was created enabled and started its schedule without the form's schedule notice applying. The handoff andCanvasGraphRequestnow carry the form'senablechoice and the canvas publishes with it; chat drafts and the edit-existing path keep the historical enabled-on-save behavior (the store only appliesenableon first create). Regression tests: newConductorCanvas.test.tsx(stubbed@xyflow/react) reproducing the exact scenario, plus extended form handoff tests.Design decisions
resolveendpoint) instead of sharing@omadia/conductor-corewith web-ui — one code path serves both the UI and omadia Facilitator — agent that moderates group chats to a defined outcome (+ Conductor JIT/ephemeral workflows & Core group primitives) #330's ephemeral instantiation.Testing
middleware: typecheck + full test suite green (conductor-core template unit tests, catalog CI gate, route tests).web-ui:tsc --noEmitclean, 37 test files / 230 tests green (incl. new gallery, form, and canvas-handoff tests), i18n parity check OK.Post-review hardening (review round 3, cross-vendor findings)
The cross-vendor (codex) reviewer rejected the branch a first reviewer had approved, with two findings — both fixed and re-approved by both reviewers:
8f25c15): thegetBySlug()pre-check +ON CONFLICT DO UPDATEcombination was a TOCTOU — two concurrentinstantiatecalls with the same fresh slug both returned 201, the loser silently publishing a second version over the winner.createOrPublishnow takesexpectNew, flipping the conflict clause toINSERT ... ON CONFLICT (slug) DO NOTHING; zero returned rows rolls the transaction back and throwsWorkflowSlugExistsError, which the route maps to the 409conductor.slug_existsenvelope. The pre-check is gone; manualPOST /and the canvas save path provably keep upsert semantics (regression tests pin both conflict-clause modes).a0a3df3): template name, description, use-case tag, slot labels and help texts are nowLocalizedText(stringor{ en, de? },enrequired and validated bycheckTemplateManifest). All four bundled templates ship full German translations, gated by the catalog CI test. The web-ui resolves the active locale (useLocale) with en fallback;GET /templateskeeps returning unresolved full manifests so the omadia Facilitator — agent that moderates group chats to a defined outcome (+ Conductor JIT/ephemeral workflows & Core group primitives) #330 machine contract stays stable. Localization lives in the manifest — not inen.json/de.json— so it travels with templates once v2 (Conductor workflow templates v2: save-as-template, distribution, DB storage, discovery, chat awareness, telemetry, text slots, versioning #478) distributes them outside the repo.Closes #429