agentsclimarketplace

Transform

Skill exmergo/dex/skills/transform

Dex is the agent-native analytics engineering toolkit. Point it at your warehouse and your dbt project. It learns the landscape, authors your transformations, and tells you exactly what to fix when the schema drifts. Built for analytics engineers and data engineers who want more out of their coding agent.

Install
npx -y skills add exmergo/dex --skill transform

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 15 stars15 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Use this to author and change a dbt project: bootstrap a new dbt project in a repo that has none (`transform init`), write or refactor dbt model SQL from staging to marts, add tests and docs in schema.yml, manage dependencies, and define or update the semantic layer (dbt semantic models / MetricFlow: entities, dimensions, measures, metrics). Trigger it for requests like "set up a dbt project in this repo", "build a staging model for this table", "refactor this model", "add tests to this model", "create a mart for X", "define a revenue metric", or "add a dimension to this entity". Every change is a reviewable diff to the dbt project; any warehouse build is dev-target only, gated, and cost-surfaced first. Do not use it to explore or profile a warehouse (use explore) or to detect drift and reconcile a project that has fallen out of sync (use maintain).

SKILL.md

14.6 KB, as published. Nobody here has run it

Transform

Author and refactor the dbt project: both the SQL transformations (staging to marts, tests, docs) and the semantic layer on top (entities, dimensions, measures, metrics). Both are the same job, writing reviewable diffs to the dbt project, which is the source of truth. This is the building half of the loop. It writes only to the repo, as reviewable diffs, and runs against a dev target only.

How to drive it

uv run "${CLAUDE_SKILL_DIR}/scripts/run.py" <subcommand> [flags]

You author the dbt file content; the engine validates it, computes the diffs, and stores the proposal as a plan. Hand content over with --edits-file <path> (or - to read stdin), a JSON payload:

{"edits": [
  {"path": "models/staging/stg_orders.sql", "kind": "model_sql", "content": "..."},
  {"path": "models/staging/stg_orders.yml", "kind": "schema_yml", "content": "..."},
  {"path": "models/marts/dim_orders.sql", "kind": "model_sql", "op": "delete"}
]}

kind is model_sql, schema_yml, semantic_yml (optional on semantic define|update|plan, which imply it), macro_sql (a macro file under the project's macro paths), packages_yml, project_yml (the project-root dbt_project.yml), or profiles_yml (the project-root profiles.yml). Model SQL must be a single read-only SELECT once its jinja is stripped; semantic YAML is validated against MetricFlow's schemas, cross-reference-checked, and (when dbt is available) parsed by dbt itself before the plan is accepted; a macro file must hold only macro definitions and jinja comments. project_yml must keep a name; profiles_yml must reference every secret via {{ env_var('NAME') }} (a literal credential is refused so none reaches the diff), and both config kinds are parsed by dbt at plan time.

op is upsert (the default: create or update, carrying content) or delete (remove the file, no content). A delete is a first-class reviewable diff like any other edit, so a reclassification or refactor is one plan rather than a plan plus a manual rm. Deletes are guarded: the plan is refused if any file that survives it still ref()s a deleted model, naming the offenders. Carry the edits that remove those references in the same plan (for a rename, delete the old model, create the new one, and update every referrer to point at it, all together) so the post-change project is validated as one unit. An unconfirmed delete against a file a human edited after planning surfaces as needs_confirmation, never a silent removal.

Bootstrapping a project

If no dbt project exists in the repo, offer transform init before anything else: transform plan needs a project to edit. Ask the user for the project name and confirm the connector with them, then run:

uv run "${CLAUDE_SKILL_DIR}/scripts/run.py" transform init "<name>" --connector <c>

The engine renders the whole skeleton (dbt_project.yml, models/staging/ and models/marts/, a profiles.yml with a single dev target and no secrets) and records connector, dbt_project_dir, and dbt_target: dev in .dex/config.yml; do not hand-write these files yourself. Init never assumes a connector: it errors rather than defaulting, so always pass the user's confirmed choice (a connector: already committed in .dex/config.yml also counts). Every connector is supported: DuckDB, BigQuery, Snowflake, Databricks, Postgres, and Redshift. DuckDB needs a warehouse path (--path, or the duckdb.path config). BigQuery needs a GCP project (usually bigquery.project in .dex/config.yml; confirm it with the user) and writes builds to a dedicated dev dataset (bigquery.dev_dataset, default dbt_dev); auth is Application Default Credentials, so if credentials are missing tell the user to run gcloud auth application-default login, never ask for a key. Snowflake writes builds to a dedicated snowflake.dev_database/dev_schema on the pinned warehouse; Databricks writes builds to a dedicated databricks.dev_catalog/dev_schema on the pinned SQL warehouse (if credentials are missing tell the user to run databricks auth login, never ask for a token); Postgres writes builds to a dedicated postgres.dev_schema (default dbt_dev), with the password reaching dbt only through the PGPASSWORD environment variable. Redshift writes builds to a dedicated redshift.dev_schema (default dbt_dev): with a redshift.workgroup pinned the profile renders IAM auth (temporary credentials from the AWS chain, nothing persisted), otherwise the password reaches dbt only through the REDSHIFT_PASSWORD environment variable. All of them discover their connections and refuse with the fix named when none resolves. Init refuses if any dbt project already exists.

When the user wants staging/intermediate/marts isolated in their own datasets/schemas (a common ask when the warehouse is shared with unrelated work), offer --layered-schemas: init then also scaffolds models/intermediate/, a generate_schema_name override, and per-folder +schema: config, so builds land in staging_dev / intermediate_dev / marts_dev instead of one shared dev namespace. Do not hand-write that macro; existing projects can adopt it later via transform macro generate_schema_name. Note dbt warns about "unused configuration paths" until the first model lands in each layer folder; that resolves itself.

Init also checks (free, metadata-only) whether each namespace the project would build into already exists with content, and warns naming the namespace and a few object names. The warning is advisory: relay it to the user and ask whether the content is theirs (a previous dev build) or unrelated; when it is unrelated, discard the freshly scaffolded project (nothing has been built), point the config at a different dev namespace, and re-run init. A "could not check" note just means no connection was reachable at init time.

dbt SQL models

  • transform plan "<intent>" --edits-file <path|-> validates the edits and returns them as diffs with a plan id. Nothing is applied yet. Add --scaffold <table> (repeatable) to generate a staging skeleton (stg_<table>.sql plus per-model YAML with key tests and PII meta) from the .dex/ cache instead of, or on top of, hand-authored edits.
  • transform apply [plan-id] writes the plan into the dbt project (the latest unapplied plan when no id is given; any plan kind, semantic included). The result is still a reviewable git diff for the user. If a human edited a file after the plan was made, nothing is written: the divergence comes back as diffs with needs_confirmation, and you should re-plan against current state (or, only when the user says so, re-run with --confirm).
  • transform plans lists stored plans (pending and applied, newest first), so you never need to browse .dex/plans/ by hand.
  • transform build --target dev runs dbt build against a dev target. The engine surfaces a cost preflight first and runs only with --confirm (plus a --budget on billed connectors). On BigQuery there is no upfront estimate (dbt has no dry-run), so always get an explicit byte budget from the user and pass it as --budget <bytes>; never invent one. Each statement dbt runs is capped server-side by the profile's maximum_bytes_billed, and the envelope reports billed bytes afterward. Production-looking targets are refused outright; --confirm cannot override that. dbt runs with its working directory pinned to the project dir, so relative paths in profiles.yml resolve against the project. When the project declares packages (packages.yml) and dbt_packages/ is missing, the engine runs dbt deps automatically before the build.
  • transform deps installs dbt packages explicitly (also the refresh path when dbt_packages/ exists but is stale). No confirmation needed: deps writes only inside the project and never touches the warehouse.

Shipped macros

  • transform macro lists the macros dex ships; transform macro <name> proposes scaffolding one into the project's macro directory as a plan, applied with transform apply like any other. The user's copy is theirs to edit; re-running the command diffs it back against the shipped version (a warning says whether it is customized or stale), and applying that plan overwrites deliberately.

  • unpivot_json_object turns a JSON object column with dynamic keys (the NoSQL-sourced shape: a Firestore/Mongo/DynamoDB document keyed by a related entity's id) into one row per top-level key. Use it instead of hand-rolling JSON SQL; it renders a complete SELECT:

    select id, key as related_id, value as attrs
    from (
      {{ unpivot_json_object(relation=ref('stg_entities'),
                             json_column='attributes', passthrough=['id']) }}
    )
    

    The contract on every connector: one row per top-level key, key a plain string, value the warehouse's native semi-structured type (BigQuery JSON, Snowflake VARIANT, Databricks VARIANT, Postgres jsonb, Redshift SUPER, DuckDB JSON), a NULL object yields no rows, and a nested object's own field names never surface as top-level keys. For a string-typed source column pass the parse expression as json_column (parse_json(payload) on BigQuery, Snowflake, and Databricks; json_parse(payload) on Redshift); Postgres and DuckDB accept JSON-bearing text directly. Databricks needs VARIANT support (DBR 15.3+ or a current SQL warehouse). Two BigQuery quirks are absorbed by the macro, so do not "fix" them back in: a JSON path argument must be a compile-time literal (the macro reads values with the subscript operator, which accepts a computed key), and JSON_KEYS recurses into nested objects unless depth-limited (the macro pins depth 1). When a planned model calls the macro and the project lacks it, the plan warns and names the scaffold command; scaffold it rather than inlining a copy.

Preparing the dev target

Before the cost gate, and for free, transform build refuses two things and names the fix for each. Neither costs anything to check, so both surface on the unconfirmed call rather than after a budget has been agreed.

Config that has drifted from the profile. transform init renders .dex/config.yml into the project's profiles.yml, and dbt reads only the profile from then on. If a later config edit never reached it (a retargeted dev_database, a different warehouse), the build refuses and names both values and both files. Edit one to match the other. The engine never rewrites profiles.yml, which you may legitimately have hand-edited.

A dev target that does not exist. On Snowflake, dbt creates schemas but never databases, so a missing dev_database is refused with the CREATE DATABASE statement to run; dex will not create it for you, because its only writes are reviewable diffs inside the repo. On Postgres and Redshift, dbt creates the dev schema but only if the profile's user may, so the missing privilege is what gets refused, with the CREATE SCHEMA/GRANT statement to run. On DuckDB the dev target is a database file, and dbt would happily create an empty one, then fail every source() relation with a confusing catalog error. The convention there: copy the shared source warehouse to the dev target path (for example cp shared/f1.duckdb <project>/dev.duckdb), or point the dev target at an existing file. Projects without sources just get a warning and an empty database, which is fine for model-only builds.

The semantic layer

  • semantic define ... --edits-file <path|-> and semantic update ... author and evolve the dbt semantic models (entities, dimensions, measures, metrics) as plans. define refuses names that already exist (use update); update refuses names that do not (use define). For one logical change that mixes both (evolve existing metrics and add the helpers they depend on), use semantic plan ...: it accepts mixed intent and classifies each name, and the envelope reports the split as defined and updated.
  • Plan-time validation is layered so a plan that validates will build: MetricFlow's schemas check the shape; the engine resolves every metric input (ratio and derived metrics reference metrics, not measures; a measure only becomes a metric via create_metric: true, and the error names that fix); and finally the emitted YAML is run through dbt's own parser against a throwaway copy of the project. A plan that fails parse is refused, not stored. If dbt is not installed the parse degrades to a warning; --no-parse skips it explicitly.
  • A semantic plan is applied like any other: transform apply [plan-id] writes its YAML into the dbt project (no id applies the latest unapplied plan).
  • dbt cannot parse semantic models in a project without a MetricFlow time spine; the engine warns when one is missing and defers the parse gate until one exists. Author it like any other model (a day-grain date model plus YAML with a time_spine: config) in the same or a separate plan.
  • viz preview is not yet implemented (it returns not_implemented); the Viz integration arrives later.

Guardrails (enforced in the engine, not here)

  • Writes confined to the repo, and within it to the dbt project's model paths. dex never writes to source warehouse data.
  • Dev-target only. Prod-target execution is never initiated by dex.
  • Cost surfaced before any spend. A build that would spend requires explicit confirmation and a session budget.
  • Propose, don't impose. Human edits to dbt (SQL and semantic YAML) are authoritative; on conflict the engine surfaces a diff and asks rather than overwriting.
  • PII flags propagate from the cache into emitted dbt (model and column meta), never example values. Stamping is presence-based at any confidence; only a column cleared by a human pii_overrides entry in .dex/config.yml is scaffolded without the meta.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.