Copy workflow templates between environments¶
Last reviewed: 2026-09-09
Replicate the workflow templates (the workflow_configs rows with
is_template = TRUE, shown at Tools & Templates → Workflow Templates) from a
source environment into a target one — e.g. seed dev with the templates an
engineer built on the richmond city box.
This copies templates only, never ordinary workflows, and never overwrites a template that already exists in the target.
There is one script per target environment, each a thin wrapper over a shared core so the logic cannot drift between them:
| Environment | Script | npm script |
|---|---|---|
| dev | copy-workflow-templates-dev.ts |
copy:workflow-templates:dev |
| qa | copy-workflow-templates-qa.ts |
copy:workflow-templates:qa |
| prod | copy-workflow-templates-prod.ts |
copy:workflow-templates:prod |
Each wrapper declares its environment's RDS host token (e.g. trusrv-qa-orch)
and the run aborts unless the connected POSTGRES_HOST contains that token —
so the prod script cannot accidentally write to qa, etc. Set
ALLOW_HOST_MISMATCH=1 only for a deliberately unusual target (a local clone).
What it does¶
The shared core copy-workflow-templates-core.ts
reads a committed data file of source templates and, for each one, does exactly
what POST /api/workflow-templates does:
- Insert the
workflow_configsrow (is_template = TRUE,is_activepreserved from the source), then - create the editor draft via
create_workflow_draft(id, config, created_by), then - set
tags(andexpose_mcp/mcp_tool_namewhen the source had them),
all inside one withTenantContext transaction so RLS and the tenant GUC match
the app's own creation path.
It does not duplicate what already exists. A source template is skipped when
the target tenant already has a template with the same name (trimmed,
case-insensitive). A same-named target template with a different config is still
skipped (never overwritten) but logged as a WARN. The source list is also
de-duplicated by name first — richmond carries two byte-identical
"Email Translation Template" rows and only one is created.
It is also dependency-aware: before creating templates it recreates any missing referenced tool and reports missing referenced integrations — see Dependency handling.
Capturing the source data¶
The committed file
apps/ai-manager/scripts/workflow-templates-richmond.json
holds the seven richmond templates exported verbatim (name, description, config
JSONB, tags, is_active, expose_mcp, mcp_tool_name). To refresh it from a source
environment, export the is_template = TRUE rows as a JSON array of
{source_id, name, description, is_active, tags, expose_mcp, mcp_tool_name, config}
objects. For a city box (Overmind + a tsf-local-postgres container) reachable
via SSM:
aws ssm send-command --instance-ids <city-instance-id> \
--document-name AWS-RunShellScript \
--parameters 'commands=["docker exec tsf-local-postgres psql -U tsf_app -d trusrv_agents -tAc \"SELECT json_agg(t) FROM (SELECT id AS source_id, name, description, config, is_active, tags, expose_mcp, mcp_tool_name FROM workflow_configs WHERE is_template=TRUE ORDER BY id) t;\" | gzip -c | base64 -w0"]'
then base64 -d | gunzip the StandardOutputContent and write it to the data file.
Capturing the dependency snapshot¶
A template config references two kinds of object by UUID, and copying the
workflow_configs row alone leaves them dangling in a target that lacks them:
- Node-level
integration_id(LLM / notification nodes) → anintegrationsrow. - Tool nodes'
config.tool(a UUID) → atool_configsrow. A missing one renders as "Failed to load tool" in the editor and the node cannot run.
The committed file
apps/ai-manager/scripts/workflow-templates-richmond-dependencies.json
holds a non-secret snapshot of those referenced objects so the copy tooling
can recreate a missing referenced tool automatically (see below). Refresh it
from the source env with (city box example):
aws ssm send-command --instance-ids <city-instance-id> \
--document-name AWS-RunShellScript \
--parameters 'commands=["docker exec tsf-local-postgres psql -U tsf_app -d trusrv_agents -tAc \"SELECT json_build_object('\''tools'\'', (SELECT json_agg(json_build_object('\''id'\'', tc.id, '\''type_id'\'', tc.type_id, '\''name'\'', tc.name, '\''description'\'', tc.description, '\''config'\'', tc.config, '\''input_schema'\'', tc.input_schema, '\''output_schema'\'', tc.output_schema, '\''parameters'\'', tc.parameters, '\''expose_to_agents'\'', tc.expose_to_agents, '\''is_active'\'', tc.is_active, '\''integration'\'', (SELECT json_build_object('\''id'\'', i.id, '\''type_id'\'', i.type_id, '\''name'\'', i.name) FROM integrations i WHERE i.id = tc.integration_id), '\''secret_key_names'\'', (SELECT coalesce(array_agg(ts.key_name), '\''{}'\'') FROM tool_secrets ts WHERE ts.tool_id = tc.id))) FROM tool_configs tc WHERE tc.id = ANY(ARRAY[<referenced tool uuids>]::uuid[]))) t;\""]'
secret_key_names records which secrets the source tool held — the copy tooling
refuses to recreate a secret-bearing tool (credentials can't cross the per-env
MASTER_ENCRYPTION_KEY) and flags it for manual re-entry instead. The referenced
tool UUIDs are the distinct config.tool values across the captured templates.
Where to run it (KMS: inside the cluster)¶
On the KMS clusters (dev/qa/prod) the RDS security group only accepts ingress
from the EKS pod security group — a codespace or SSM tunnel cannot reach the
database. So these scripts run from inside the target cluster, where an
ai-manager pod already has the POSTGRES_* env populated.
The deployed ai-manager image is a production build and cannot run the
.ts wrappers directly. The app lives at /app (not /app/apps/ai-manager),
the container is named app, and the image has no npm/tsx and a
pruned node_modules (missing dev/transitive packages such as re2js that
lib/db's import chain pulls in). So npm run copy:workflow-templates:qa does
not work against a running pod. Two paths that do:
-
One-off
Jobfrom a full-toolchain image (preferred — runs the reviewed core verbatim). Use an image that carriestsx+ the fullnode_modulesand the samePOSTGRES_*/MASTER_ENCRYPTION_KEYenv as the ai-manager pod, thennode --import tsx apps/ai-manager/scripts/copy-workflow-templates-qa.ts(dry run) /… -- --apply. -
pg-only port, in a running pod (works on the deployed image as-is, becausepgis present). Reproduce the core's logic — source name-dedup,collectReferences, dependency-tool recreate,createWorkflow(is_template+ tenant INSERT),create_workflow_draft, tags/mcp — in a standalone.mjsthat loadspgviacreateRequire('/app/package.json'), sets the tenant GUCs (app.current_tenant_id+app.is_platform_admin) in one transaction, andROLLBACKs on dry run /COMMITs on--apply.kubectl cpit into/tmp(writable;/appis read-only) andnode /tmp/…mjs. This is exactly how the qa seed was run on 2026-09-09.
On the KMS clusters kubectl is read-only except through ArgoCD; running a script that writes to the DB from inside a pod is the sanctioned path for this data seed (it is a normal application write, not a manifest change).
tool_configs.created_byis auuidcolumn. The dependency-tool recreate must passNULLthere (a script has no user id), not the script name — the original core passed the filename string and threwinvalid input syntax for type uuidon the first real qa apply, since the recreate path never runs in dev (every referenced tool already exists there). Fixed incopy-workflow-templates-core.ts; apg-only port must do the same.
Running it (local / city box with a reachable DB)¶
Point the ai-manager DB connection env (POSTGRES_HOST / DATABASE_URL / PG*)
at the target database, then run the wrapper for that environment. Dry run
prints the plan and writes nothing:
cd apps/ai-manager
# Dry run:
npm run copy:workflow-templates:dev
# Apply:
npm run copy:workflow-templates:dev -- --apply
TARGET_TENANT_ID selects the tenant the templates land in (default the
TrueServe tenant 00000000-0000-0000-0000-000000000001). The scripts are
idempotent — re-running inserts only what is missing. The host guard aborts if
the connected DB does not look like the environment the wrapper targets.
Dependency handling (tools + integrations)¶
Before creating any template, the core runs a dependency pass that walks the
source configs for referenced UUIDs and reports/repairs them. It prints a DEP
line per dependency in both dry run and --apply; only --apply writes.
Referenced tools (config.tool → tool_configs). For each referenced tool
UUID that is missing in the target, the pass recreates it with the same id
(so the node's config.tool resolves) using the same non-secret column set
createTool uses, sourced from the dependency snapshot. The tool's backing
integration is resolved against the target — by id first, then by
(type_id, name) — so a target whose integration carries a different UUID
still links up; if neither matches, the tool is created with no integration
and a DEP warning tells the operator to re-select it in the node. A tool whose
snapshot lists secret_key_names is not recreated (secrets can't cross the
per-env MASTER_ENCRYPTION_KEY): it is flagged for manual creation + credential
re-entry and counted under "dependency issue(s)". A referenced tool that is
missing and absent from the snapshot is likewise flagged (refresh the
snapshot from the source env).
Referenced integrations (node-level integration_id). Integrations are
never auto-copied — they may hold secrets. The pass only reports whether
each referenced integration (node-level, plus each recreated tool's backing one)
is present in the target; a missing one is a DEP warning and those nodes carry
a dangling reference until the integration is created and re-selected. The
template's integration_id values are copied verbatim and never rewritten.
For the richmond → dev path every referenced object already exists under the same
UUIDs (both node-level integrations — 92249494-… "TrueServe Deloitte OpenAI" /
openai, ec667d5f-… "DCCDev Connect" / amazon_connect — and the SFDC DevSDO
Action tool 17983344-… with its salesforce integration), so the dependency
pass is a no-op there. For qa/prod it recreates the non-secret tool
automatically and reports any integration the operator still needs to wire.
See also¶
- Workflow templates (four template kinds, two stores)
POST /api/workflow-templates- Dependency snapshot ·
createTool(the column set the recreate mirrors)