Skip to content

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:

  1. Insert the workflow_configs row (is_template = TRUE, is_active preserved from the source), then
  2. create the editor draft via create_workflow_draft(id, config, created_by), then
  3. set tags (and expose_mcp / mcp_tool_name when 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) → an integrations row.
  • Tool nodes' config.tool (a UUID) → a tool_configs row. 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 Job from a full-toolchain image (preferred — runs the reviewed core verbatim). Use an image that carries tsx + the full node_modules and the same POSTGRES_*/MASTER_ENCRYPTION_KEY env as the ai-manager pod, then node --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, because pg is 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 .mjs that loads pg via createRequire('/app/package.json'), sets the tenant GUCs (app.current_tenant_id + app.is_platform_admin) in one transaction, and ROLLBACKs on dry run / COMMITs on --apply. kubectl cp it into /tmp (writable; /app is read-only) and node /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_by is a uuid column. The dependency-tool recreate must pass NULL there (a script has no user id), not the script name — the original core passed the filename string and threw invalid input syntax for type uuid on the first real qa apply, since the recreate path never runs in dev (every referenced tool already exists there). Fixed in copy-workflow-templates-core.ts; a pg-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