Skip to content

Transforms

Transforms modify taxonomy values when event keys are generated.

They are defined in opentp.cli.yaml as named pipelines of steps under keygen.transforms, and referenced in keygen.template as {field | pipeline}. (In 2026-01 plans they lived in opentp.yaml under spec.events.x-opentp.keygen; opentp migrate moves them.)

opentp.cli.yaml
opentp: 2026-09
keygen:
template: "{area | slug}::{event | slug}"
transforms:
slug:
- lower
- trim
- replace:
from: " "
to: "_"
- truncate: 160

Each step is either:

  • a string step name (e.g. lower)
  • a single-key object with parameters (e.g. replace: { from: " ", to: "_" })

A pipeline runs its steps in order; a placeholder with several pipelines ({event | slug | short}) runs them left to right. See the template grammar.

An unknown step (neither built in nor loaded from a plugin) or a malformed step (for example a mapping with two keys) is a configuration error. It is reported once, against opentp.cli.yaml, at keygen.transforms.<pipeline>[<index>]:

[opentp.cli.yaml]
✗ keygen.transforms.slug[0]: Unknown transform step 'slugify' (custom steps: keygen.plugins in opentp.cli.yaml, or --external-transforms)
✗ Validation failed errorCount=1 warningCount=0 eventCount=1

Until it is fixed, validate and fix exit with code 1 without generating keys (fix rewrites nothing), and generate refuses to export the plan. Unknown parameters of a known step are not reported: most steps ignore parameters they cannot use.

StepParametersExample
lowernoneSign Up → sign up
uppernoneSign Up → SIGN UP
trimnone (whitespace), or { chars: "<characters>" }trim: { chars: "_" }: __sign_up__ → sign_up
replace{ from, to }: every occurrence of the literal text fromreplace: { from: " ", to: "_" }: Sign Up Now → Sign_Up_Now
truncatea number, or { maxLen }truncate: 4: Sign Up → Sign
collapsenone: removes every character outside A-Z, a-z, 0-9Sign-Up 2! → SignUp2
keep{ chars: "<character class>" }: keeps only those characterskeep: { chars: "a-z_" }: sign up_now! → signup_now
to-snake-casenonesignUp Now → sign_up_now
to-kebabnone (does not change case)Sign Up Now → Sign-Up-Now
to-camel-casenone: lowercases, then joins on -, _ and spacessign_up now → signUpNow
to-underscorenone: every run of characters outside A-Z, a-z, 0-9 becomes _ (does not change case)Sign-Up Now → Sign_Up_Now
transliterate{ map: { <character>: <replacement> } } (lowercase keys; an uppercase source character gets a capitalized replacement)transliterate: { map: { "é": "e", "ß": "ss" } }: Café Straße → Cafe Strasse

collapse, keep (with an ASCII class), to-snake-case, to-kebab, to-underscore and to-camel-case work on ASCII letters and digits: they drop or split on every other character, so a taxonomy value written in another script can become empty. Use transliterate first (with your own map: opentp has no built-in language presets), or write a custom step.

A step is a module with a name and a factory(params) that returns a function from string to string:

tools/transforms/reverse/index.js
module.exports = {
name: "reverse",
factory: () => (value) => value.split("").reverse().join(""),
};

Load custom steps in one of two ways:

  • keygen.plugins in opentp.cli.yaml (directories relative to that file), loaded only with --allow-plugins or OPENTP_ALLOW_PLUGINS=1:

    opentp.cli.yaml
    opentp: 2026-09
    keygen:
    template: "{area | slug}::{event | slug}"
    transforms:
    slug:
    - lower
    - reverse
    plugins: [tools/transforms]
  • --external-transforms <dir> on the command line (relative to the current directory, always loaded):

    Terminal window
    opentp validate --external-transforms ./tools/transforms

Each <dir>/<name>/index.js is loaded as an ES module or a CommonJS module, depending on the nearest package.json. Steps are checked when the plan is loaded, so validate, fix, generate and mcp all need the plugins (pass --allow-plugins or the same --external-transforms to each). See Plugins and —allow-plugins.