# End-to-End Walkthrough: Transformations Management with Rudder CLI


In this tutorial, you will use the [Rudder CLI tool]({{< ref "dev-tools/rudder-cli/" >}}) to define, validate, test, and apply transformations from your local project.

## Prerequisites

- Rudder CLI tool (`rudder-cli`) [installed locally]({{< ref "dev-tools/rudder-cli/installation.md" >}})
- A project directory with your transformation YAML files
- In your RudderStack workspace, create a [workspace-level Service Access Token]({{< ref "access-management/service-access-tokens.md#workspace-sat" >}}) with the following permissions:

| Resource | Permissions |
| :----| :-----|
| Transformations | **Edit**, **Connect**, **Create & Delete** |
| Transformation Libraries | **Edit** |

{{< customreadfile "/includes/rudder-cli/token-auth-footer-transformations.md" >}}

## 1. Authenticate the CLI tool

{{< customreadfile "/includes/rudder-cli/auth-login-step.md" >}}

## 2. Create a project directory

Create a project directory to store your transformation specs, code, and tests:

```shell
mkdir -p ~/tutorial-transformations/transformations
```

Example structure:

```text
tutorial-transformations/
└── transformations/
    ├── my-transformation.yaml
    ├── my-library.yaml
    ├── my-python-transformation.yaml
    ├── my-python-library.yaml
    ├── javascript/
    │   ├── my-transformation.js
    │   └── my-library.js
    ├── python/
    │   ├── my-python-transformation.py
    │   └── my-python-library.py
    └── tests/
        ├── input/
        │   └── product_clicked.json
        └── output/
            └── product_clicked.json
```

{{< announcement >}}
RudderStack supports Python transformation libraries only in the [Growth](https://rudderstack.com/pricing/) and [Enterprise](https://www.rudderstack.com/enterprise-quote/) plans.
{{< /announcement >}}

## 3. Add a transformation spec

This section lists the steps to create a transformation YAML spec via the following methods:

- [Referencing an external file](#referencing-an-external-file)
- [Using inline code](#using-inline-code)

#### Referencing an external file

1. Create the YAML spec for your transformation:

{{< tabs tabTotal="2" >}}
{{% tab tabName="JavaScript" %}}
`~/tutorial-transformations/transformations/my-transformation.yaml`:

```yaml
version: rudder/v1
kind: transformation
metadata:
  name: my-transformation
spec:
  id: my-transformation
  name: My Transformation
  description: Add static metadata to each incoming event
  language: javascript
  file: javascript/my-transformation.js
  tests:
    - name: Basic payload test
      input: tests/input
      output: tests/output
```
{{% /tab %}}
{{% tab tabName="Python" %}}
`~/tutorial-transformations/transformations/my-python-transformation.yaml`:

```yaml
version: rudder/v1
kind: transformation
metadata:
  name: my-python-transformation
spec:
  id: my-python-transformation
  name: My Python Transformation
  description: Tag each incoming event with a processing timestamp
  language: python
  file: python/my-python-transformation.py
  tests:
    - name: Basic payload test
      input: ./tests/input
      output: ./tests/output
```
{{% /tab %}}
{{< /tabs >}}

2. Create the transformation code file:

{{< tabs tabTotal="2" >}}
{{% tab tabName="JavaScript" %}}
`~/tutorial-transformations/transformations/javascript/my-transformation.js`:

```javascript
export function transformEvent(event, metadata) {
  event.context = event.context || {};
  event.context.cliManaged = true;
  return event;
}
```
{{% /tab %}}
{{% tab tabName="Python" %}}
`~/tutorial-transformations/transformations/python/my-python-transformation.py`:

```python
from datetime import datetime, timezone

def transformEvent(event, metadata):
    event.setdefault("context", {})
    event["context"]["processedAt"] = datetime.now(timezone.utc).isoformat()
    return event
```
{{% /tab %}}
{{< /tabs >}}

#### Using inline code

You can also write transformation code directly in the YAML spec using `code` instead of referencing an external file with `file`:

{{< tabs tabTotal="2" >}}
{{% tab tabName="JavaScript" %}}
```yaml
version: rudder/v1
kind: transformation
metadata:
  name: inline-transformation
spec:
  id: inline-transformation
  name: Inline Transformation
  description: Add processing metadata to each event
  language: javascript
  code: |
    export function transformEvent(event, metadata) {
      event.context = event.context || {};
      event.context.processedBy = "inline-transformation";
      return event;
    }
```
{{% /tab %}}
{{% tab tabName="Python" %}}
```yaml
version: rudder/v1
kind: transformation
metadata:
  name: inline-python-transformation
spec:
  id: inline-python-transformation
  name: Inline Python Transformation
  description: Add processing metadata to each event
  language: python
  code: |
    def transformEvent(event, metadata):
        event.setdefault("context", {})
        event["context"]["processedBy"] = "inline-python-transformation"
        return event
```
{{% /tab %}}
{{< /tabs >}}

{{< info >}}
`code` and `file` are mutually exclusive — use one or the other in a given spec. See the [Transformation YAML Reference]({{< ref "dev-tools/rudder-cli/yaml-transformations.md" >}}) for the complete schema.
{{< /info >}}

## 4. Add a transformation library spec

Create the YAML spec for your transformation library:

{{< tabs tabTotal="2" >}}
{{% tab tabName="JavaScript" %}}
`~/tutorial-transformations/transformations/my-library.yaml`:

```yaml
version: rudder/v1
kind: transformation-library
metadata:
  name: my-library
spec:
  id: my-library
  name: my library
  description: Shared helper utilities
  language: javascript
  file: javascript/my-library.js
  import_name: myLibrary
```
{{% /tab %}}
{{% tab tabName="Python" %}}
`~/tutorial-transformations/transformations/my-python-library.yaml`:

```yaml
version: rudder/v1
kind: transformation-library
metadata:
  name: my-python-library
spec:
  id: my-python-library
  name: my python library
  description: Shared Python helper utilities
  language: python
  file: python/my-python-library.py
  import_name: myPythonLibrary
```
{{% /tab %}}
{{< /tabs >}}

Then, create the library code file:

{{< tabs tabTotal="2" >}}
{{% tab tabName="JavaScript" %}}
`~/tutorial-transformations/transformations/javascript/my-library.js`:

```javascript
export function addTag(event, key, value) {
  event.context = event.context || {};
  event.context[key] = value;
  return event;
}
```
{{% /tab %}}
{{% tab tabName="Python" %}}
`~/tutorial-transformations/transformations/python/my-python-library.py`:

```python
def add_tag(event, key, value):
    event.setdefault("context", {})
    event["context"][key] = value
    return event
```
{{% /tab %}}
{{< /tabs >}}

{{< info >}}
For library resources, `import_name` must be the camelCase form of `name`. For example, `name: my library` maps to `import_name: myLibrary` and `name: my python library` maps to `import_name: myPythonLibrary`.
{{< /info >}}

## 5. Validate YAML and code references

Run validation from the project root:

```shell
rudder-cli validate -l ~/tutorial-transformations
```

This command validates required fields, `code`/`file` rules, supported languages, code syntax, and test definitions.

## 6. Test transformations locally

Rudder CLI supports 3 test modes:

{{< warning >}}
You cannot include multiple test modes in a single command.
{{< /warning >}}

1. Test a specific transformation by ID:

```shell
rudder-cli transformations test my-transformation -l ~/tutorial-transformations
```

2. Test all transformations:

```shell
rudder-cli transformations test --all -l ~/tutorial-transformations
```

3. Test only new or modified transformations:

```shell
rudder-cli transformations test --modified -l ~/tutorial-transformations
```

**Optional**

- Show detailed failures and diffs:

```shell
rudder-cli transformations test --all --verbose -l ~/tutorial-transformations
```

- Show the built-in default events:

```shell
rudder-cli transformations show-default-events
```

#### How it works

When running tests, Rudder CLI loads input files from each test suite's `input` path. If a matching output file (same filename) exists in the `output` path, it compares the actual output against the expected output. If no matching output file exists, the test still runs but skips the output comparison.

{{< info >}}
Rudder CLI tests the transformation using default RudderStack events if:

- `spec.tests` isn't configured, or
- No input JSON files are found in the configured input paths
{{< /info >}}

See the [Rudder CLI transformations testing framework]({{< ref "dev-tools/rudder-cli/yaml-transformations.md#testing-framework" >}}) section for more details.

## 7. Apply changes

1. Optional dry-run:

```shell
rudder-cli apply -l ~/tutorial-transformations --dry-run
```

2. Apply to workspace:

```shell
rudder-cli apply -l ~/tutorial-transformations
```

## Next steps

- See the [Transformation YAML Reference]({{< ref "dev-tools/rudder-cli/yaml-transformations.md" >}}) for detailed resource schemas and constraints
- See [Manage Transformations Using Rudder CLI]({{< ref "dev-tools/rudder-cli/manage-transformations.md" >}}) for the overall CLI workflow
- Automate with [GitHub Actions]({{< ref "dev-tools/rudder-cli/github-actions/transformations.md" >}})

