Skip to content
rhannekenPublic

About

A migration tool for Genesys Cloud flows

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

166 Commits

Folders and files

Repository files navigation

@rhanneken/flowy

A migration tool for Genesys Cloud flows, inspired by Flyway and Phinx.

Flowy handles authentication, SDK session management, history tracking, and error handling. You write only the logic that changes your flows.


Installation

npm install -g @rhanneken/flowy

Node.js 18 or later is required.


Quick start

# 1. Scaffold a config file in your project directory
flowy init

# 2. Edit flowy.config.js with your credentials
# 3. Create your first migration
flowy create "add greeting prompt"

# 4. Edit the generated migration file, then apply it
flowy migrate

Project structure

Flowy reads flowy.config.js and the migrations/ folder from your current working directory. Nothing from flowy lives in your project — only your config and migration files.

my-genesys-project/
├── .env
├── flowy.config.js
└── migrations/
    ├── V001__add_greeting_prompt.js
    └── V002__add_callback_menu.ts

Configuration

flowy init creates a starter flowy.config.js. Edit it to match your environments:

// flowy.config.js
module.exports = {
  migrationsDir: './migrations',   // default; can be omitted
  defaultEnvironment: 'dev',

  environments: {
    dev: {
      clientId:     process.env.GC_CLIENT_ID,
      clientSecret: process.env.GC_CLIENT_SECRET,
      region:       'mypurecloud.com',
    },
    prod: {
      clientId:     process.env.GC_CLIENT_ID_PROD,
      clientSecret: process.env.GC_CLIENT_SECRET_PROD,
      region:       'mypurecloud.com',
    },
  },
};

Flowy loads .env automatically via dotenv. The --env flag on any command overrides defaultEnvironment. Credentials always come from environment variables — never hardcode them in the config file.

Running behind a proxy

If flowy runs on a network that requires an outbound HTTP(S) proxy (common on internal/firewalled CI runners), set HTTP_PROXY / HTTPS_PROXY / NO_PROXY in the environment. This is required even if purecloud-platform-client-v2 (the REST client) already works without it — the Architect Scripting SDK makes its own raw requests and does not auto-detect a proxy the way purecloud-platform-client-v2 (via axios) does, so flowy migrate/flowy rollback can hang or fail with a certificate error even when other Genesys Cloud API calls succeed.

GLOBAL_AGENT_HTTP_PROXY / GLOBAL_AGENT_HTTPS_PROXY / GLOBAL_AGENT_NO_PROXY are also honored, as a fallback for whichever of the standard names isn't set.

If your proxy blocks by hostname allowlist, the Architect Scripting SDK needs egress to apps.<region> (e.g. apps.usw2.pure.cloud) for session setup and streaming.<region> for its live WebSocket connection, in addition to whatever api.<region> / login.<region> access the REST client already needs.


Migration files

A migration can be either a single file or a directory:

  • File: V<NNN>__<description>.js (or .ts)
  • Directory: V<NNN>__<description>/ containing an index.js (or index.ts) entry point

Both styles coexist freely in the same migrations/ folder. Use a directory when a migration involves multiple files — helper modules, audio assets, prompt scripts, etc. Files inside the directory are available via normal relative require/import. Checksums cover all files in the directory, so modifying any file (including assets) triggers the usual checksum warning.

Note: OS-generated bookkeeping files are always excluded from directory checksums: .DS_Store, ._* (macOS AppleDouble), .Spotlight-V100, .Trashes, Thumbs.db, and desktop.ini. You don't need to .gitignore them out of the migration folder for flowy's sake, though you likely already do for git's.

Each migration (file or index.js) exports:

Export Required Description
description ✓ Human-readable string stored in migration history
up(scripting, platformClient, params) ✓ Applies the migration
down(scripting, platformClient, params) Rolls back the migration (required for flowy rollback)
flows Array of { name, type } objects identifying flows to verify are unlocked before up() runs; halts migration if any listed flow is locked

You are responsible for calling checkInAsync() or publishAsync() inside up(). Flowy does not call them on your behalf.

The scripting argument

The first argument to up() and down() is the full purecloud-flow-scripting-api-sdk-javascript module, authenticated and ready to use. The parts you'll use most:

Property What it is
scripting.factories.archFactoryFlows Load, create, and check out flows
scripting.environment.archSession The active session (auth token, org info, etc.)
scripting.viewModels.flows Flow view model definitions

Consult the Architect Scripting SDK documentation for the full API reference.

JavaScript example

// migrations/V001__add_greeting_prompt.js
module.exports = {
  description: 'Add greeting prompt to main inbound flow',

  // Optional. Flowy verifies these flows are unlocked before calling up().
  // Halts with a clear error if any listed flow is locked.
  flows: [{ name: 'MainInbound', type: 'inboundcall' }],

  async up(scripting, platformClient) {
    const flows = scripting.factories.archFactoryFlows;
    const flow = await flows.checkoutAndLoadFlowByFlowNameAsync('MainInbound', 'inboundcall');
    // ... make changes to flow ...
    await flow.publishAsync();  // validate, save, and publish — releases the lock
  },

  async down(scripting, platformClient) {
    const flows = scripting.factories.archFactoryFlows;
    const flow = await flows.checkoutAndLoadFlowByFlowNameAsync('MainInbound', 'inboundcall');
    // ... undo changes ...
    await flow.checkInAsync();  // check in only (no publish) — releases the lock
  },
};

Directory migration example

migrations/
└── V002__add_hold_music/
    ├── index.js          ← entry point with up() and down()
    ├── params.js         ← optional: environment-specific values (see below)
    └── hold-music.wav    ← audio asset uploaded inside up()
// migrations/V002__add_hold_music/index.js
const { join } = require('path');

module.exports = {
  description: 'Upload hold music prompt',

  async up(scripting, platformClient) {
    const audioPath = join(__dirname, 'hold-music.wav');
    // ... upload audioPath via platformClient ...
  },

  async down(scripting, platformClient) {
    // ... delete the uploaded prompt ...
  },
};

Migration parameters (directory migrations only)

When a migration needs values that differ between environments — phone numbers, queue IDs, webhook URLs — add an optional params.js to the migration directory:

migrations/
└── V003__configure_routing/
    ├── index.js
    └── params.js

params.js exports a plain object keyed by environment name, matching the names in flowy.config.js. Non-sensitive values can be hardcoded; sensitive values should reference process.env:

// migrations/V003__configure_routing/params.js
module.exports = {
  sandbox: {
    inboundPhoneNumber: '+15550000001',
    webhookUrl: process.env.V003_ROUTING_WEBHOOK_SANDBOX,
  },
  prod: {
    inboundPhoneNumber: '+15550000002',
    webhookUrl: process.env.V003_ROUTING_WEBHOOK_PROD,
  },
};

Flowy resolves the active environment's sub-object and passes it to up() and down() as a third argument:

// migrations/V003__configure_routing/index.js
module.exports = {
  description: 'Configure inbound call routing',
  flows: [{ name: 'MainInbound', type: 'inboundcall' }],

  async up(scripting, platformClient, params) {
    const flows = scripting.factories.archFactoryFlows;
    const flow = await flows.checkoutAndLoadFlowByFlowNameAsync('MainInbound', 'inboundcall');
    // use params.inboundPhoneNumber, params.webhookUrl, etc.
    await flow.publishAsync();
  },

  async down(scripting, platformClient, params) {
    // params is available here too
  },
};

If params.js is absent, or the active environment has no matching key, params is undefined. Existing migrations that ignore the third argument are unaffected.

In TypeScript, annotate params explicitly to get type checking on the values you use:

// migrations/V003__configure_routing/index.ts
import type { FlowMigration } from '@rhanneken/flowy/types/FlowMigration';
import type { ArchitectScripting } from 'purecloud-flow-scripting-api-sdk-javascript';

interface RoutingParams {
  inboundPhoneNumber: string;
  webhookUrl: string | undefined;
}

const migration: FlowMigration<RoutingParams> = {
  description: 'Configure inbound call routing',
  flows: [{ name: 'MainInbound', type: 'inboundcall' }],

  async up(scripting: ArchitectScripting, platformClient: any, params?: RoutingParams): Promise<void> {
    const flows = scripting.factories.archFactoryFlows;
    const flow = await flows.checkoutAndLoadFlowByFlowNameAsync('MainInbound', 'inboundcall');
    // use params?.inboundPhoneNumber, params?.webhookUrl, etc.
    await flow.publishAsync();
  },

  async down(scripting: ArchitectScripting, platformClient: any, params?: RoutingParams): Promise<void> {
    // params is available here too
  },
};

export default migration;

If you don't need strong typing on the params shape, flowy create --dir --ts generates the simpler params?: Record<string, unknown> annotation, which you can narrow later.

Environment variables referenced in params.js should live in the project's top-level .env file (already gitignored). Prefixing them with the migration version (e.g. V003_ROUTING_WEBHOOK_SANDBOX) makes it easy to find and clean up variables when a migration is retired.

Note: params.js is excluded from the migration's checksum. Updating params values after a migration is applied does not trigger a checksum warning.

TypeScript example

// migrations/V002__add_callback_menu.ts
import type { FlowMigration } from '@rhanneken/flowy/types/FlowMigration';
import type { ArchitectScripting } from 'purecloud-flow-scripting-api-sdk-javascript';

const migration: FlowMigration = {
  description: 'Add callback menu to support flow',
  flows: [{ name: 'SupportInbound', type: 'inboundcall' }],

  async up(scripting: ArchitectScripting, platformClient: any): Promise<void> {
    const flows = scripting.factories.archFactoryFlows;
    const flow = await flows.checkoutAndLoadFlowByFlowNameAsync('SupportInbound', 'inboundcall');
    // ... make changes to flow ...
    await flow.publishAsync();  // validate, save, and publish — releases the lock
  },

  async down(scripting: ArchitectScripting, platformClient: any): Promise<void> {
    const flows = scripting.factories.archFactoryFlows;
    const flow = await flows.checkoutAndLoadFlowByFlowNameAsync('SupportInbound', 'inboundcall');
    // ... undo changes ...
    await flow.checkInAsync();  // check in only (no publish) — releases the lock
  },
};

export default migration;

The FlowMigration interface and the ArchitectScripting type are both optional — you can omit them and use any if you prefer less verbosity. platformClient is typed as any because the Platform Client SDK's TypeScript module declaration does not export its API classes.

TypeScript support requires tsx as an optional peer dependency:

npm install -g tsx

Projects using only .js migrations have no TypeScript dependency.


Commands

Command Description
flowy init Scaffold flowy.config.js in the current directory
flowy create <description> Create the next migration file
flowy create --ts <description> Create a TypeScript migration file
flowy create --dir <description> Create a migration directory with an index.js entry point; generated up()/down() include the params argument
flowy create --dir --ts <description> Create a migration directory with an index.ts entry point; generated up()/down() include the params argument
flowy migrate Apply all pending migrations
flowy migrate --target V005 Apply migrations up to and including V005
flowy migrate --strict Fail (rather than warn) on checksum mismatches
flowy migrate --scratch V006 Apply a single migration without recording it (local iteration)
flowy rollback Undo the last applied migration
flowy rollback --scratch V006 Run a single migration's down() without recording it (local iteration)
flowy status Show applied, pending, and failed migrations
flowy validate Check for missing version numbers or structural errors (local only)
flowy repair Fix history table problems interactively
flowy baseline Mark all existing migrations as applied without running them
flowy baseline --target V005 Baseline migrations up to and including V005
flowy unlock <flow-name> Force-unlock a flow left locked by a failed migration

All commands that communicate with Genesys Cloud accept --env <name> to override defaultEnvironment.


How it works

Single session

Flowy authenticates once at the start of flowy migrate and runs all pending migrations inside a single Architect Scripting session. The session and an authenticated Platform API Client are passed into each up() call.

History tracking

Flowy tracks applied migrations in a Genesys Cloud Data Table named _flowy_migrations, created automatically on first run. The OAuth client used by flowy needs the architect scope and Data Table read/write permissions.

Each row records the version, description, filename, a SHA-256 checksum of the migration file, timestamp, who applied it, execution time, and status (applied, failed, or rolled_back).

Checksum validation

When you run flowy migrate, flowy recomputes checksums for all previously applied migration files and compares them to what was stored at apply time. If a file has changed, flowy warns and asks for confirmation before proceeding. Use --strict to fail outright instead.

Scratch mode (local iteration)

While developing a migration that hasn't been merged yet, you often want to run its up() and down() repeatedly to get it right. Recording those trial runs in the shared _flowy_migrations table would pollute everyone's history with a version that doesn't exist on their branch. Scratch mode lets you iterate without writing any history:

flowy migrate  --scratch V006   # run V006's up(),   record nothing
flowy rollback --scratch V006   # run V006's down(), record nothing
# tweak the migration and repeat

--scratch takes the version to act on as its value (it runs that one migration, not all pending), and:

  • Writes no rows to the history table — not applied, not failed, not rolled_back. The shared ledger stays an honest record of only applied, merged migrations.
  • Refuses any version already recorded as applied. A scratch up() would re-run merged work against the org unrecorded; a scratch down() would revert merged work while history still claimed it was live. Either way the ledger would drift, so flowy stops you. To re-run an applied migration, roll it back normally first, or write a corrective migration.
  • Otherwise behaves exactly like a normal run: the pre-migration lock check, the Architect Scripting session, and your up()/down() all execute for real.

Scratch is not a dry run. The flow changes it makes are real and permanent against the target org — it simply doesn't write them down. Because nothing is recorded, the next ordinary flowy migrate will run that up() again, so a working down() (or an idempotent up()) is what makes the iterate loop repeatable. Point --scratch at a development/sandbox org, never production.

Pre-migration lock check

If a migration exports a flows array, flowy verifies each listed flow is unlocked before calling up(). If any flow is locked — by a user or a previous failed migration run — flowy halts with a clear error and instructions to resolve the lock before retrying.

Error handling

If up() throws, flowy records the migration as failed in history, logs the error, and halts. No subsequent migrations run. The exit code is non-zero so CI pipelines fail loudly.

If up() succeeds but writing to the history table fails, flowy logs a prominent warning and tells you to run flowy repair.

SDK log output and credentials

The Architect Scripting SDK writes progress notes to the console. During session start, two of those notes include the OAuth client secret and the access token. Flowy intercepts everything the SDK logs through its logging service, replaces those values with [REDACTED], and prints everything else unchanged, so you still see the SDK's notes, warnings, and errors.

Versions before 0.8.4 printed these notes verbatim. If you ran flowy migrate or flowy rollback with an earlier version, rotate the client secret of every OAuth client you used, and check anywhere the output may have been kept — CI job logs in particular. The access token in that output expires on its own; the client secret does not.

The redaction covers what the SDK logs. Anything your own migration code prints is not filtered. Don't call archLogging.setLoggingCallback from a migration: it replaces flowy's callback, and with it the redaction, for the rest of that run.

Locked flows

The Architect Scripting SDK acquires an exclusive lock on a flow when you check it out. The lock is released when publishAsync() or checkInAsync() completes. If a migration fails between checkout and the final publish/check-in, the lock is left dangling — no subsequent session can check out that flow until the lock is cleared.

To release a stranded lock:

flowy unlock "My Flow Name"

Then reset the failed migration and retry:

flowy repair   # reset the failed migration to pending
flowy migrate

Note: flowy unlock calls the Genesys Cloud force-unlock API, which requires Architect Admin permissions on the OAuth client.

flowy repair

The escape hatch for history table problems:

  1. Reset a failed entry — resets a failed migration to pending so the next flowy migrate retries it
  2. Update a checksum — clears a checksum mismatch warning after a migration file was legitimately edited post-apply
  3. Create a missing entry — records a migration that was applied successfully but never written to the history table

Exit codes

Code Meaning
0 Success
1 Migration failed
2 Configuration error
3 History store error (Data Table unavailable or permission denied)

License

BSD Zero Clause License — do whatever you want with it.

About

A migration tool for Genesys Cloud flows

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages