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

<p>In this tutorial, you will use the <a href="https://www.rudderstack.com/docs/dev-tools/rudder-cli/" >Rudder CLI tool</a> to define, validate, test, and apply transformations from your local project.</p>
<!-- end-chunk -->
<!-- begin-chunk -->
<h2 id="prerequisites">Prerequisites</h2><ul>
<li>Rudder CLI tool (<code>rudder-cli</code>) <a href="https://www.rudderstack.com/docs/dev-tools/rudder-cli/installation/" >installed locally</a></li>
<li>A project directory with your transformation YAML files</li>
<li>In your RudderStack workspace, create a <a href="https://www.rudderstack.com/docs/access-management/service-access-tokens/#workspace-sat" >workspace-level Service Access Token</a> with the following permissions:</li>
</ul>
<table>
  <thead>
      <tr>
          <th>Resource</th>
          <th>Permissions</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Transformations</td>
          <td><strong>Edit</strong>, <strong>Connect</strong>, <strong>Create &amp; Delete</strong></td>
      </tr>
      <tr>
          <td>Transformation Libraries</td>
          <td><strong>Edit</strong></td>
      </tr>
  </tbody>
</table>

<ul>
<li><strong>If you&rsquo;re on Free or self-hosted plan, or for testing and development only</strong>: Generate a <a href="https://www.rudderstack.com/docs/access-management/personal-access-tokens/" >Personal Access Token</a> with <strong>Read-Write</strong> role</li>
</ul>

<blockquote class="warning">
  <div class="tip-quote">
    
    <div class="tip-text">Any action authenticated by a Personal Access Token will break if the user generating the token is removed from the organization or there is a breaking change to their permissions.</div>
  </div>
</blockquote>
<p><strong>Token permissions for legacy RBAC system</strong></p>
<p>If you are on the <a href="https://www.rudderstack.com/docs/archive/dashboard-guides/user-management/" >legacy Permissions Management (RBAC) system</a>, your workspace-level Service Access Token should have the <strong>Admin</strong> role and <strong>Grant edit access</strong> toggled on under <strong>Transformations</strong>.</p>
<p>See <a href="https://www.rudderstack.com/docs/archive/dashboard-guides/service-access-tokens/#generate-service-access-token" >Generate a workspace-level Service Access Token</a> for steps to create the token.</p>

<figure class="image--main "  >
    <a 
         href="/docs/images/access-management/permissions/legacy/admin-transformations.webp"
        
        >
        <img src="/docs/images/access-management/permissions/legacy/admin-transformations.webp" 
         alt="workspace-level Service Access Token with Transformations Admin permission"  
         
         
        decoding="async" loading="lazy" class="img-shortcode"/>
    </a>
    
</figure>

<!-- end-chunk -->
<!-- begin-chunk -->
<h2 id="1-authenticate-the-cli-tool">1. Authenticate the CLI tool</h2>

<p>Run the following command and enter your <a href="#prerequisites" >access token</a> when prompted:</p>
<div class="rs-code">
  <div class="rs-code__head">shell<button class="rs-code__copy" type="button">
      
      Copy
    </button>
  </div>
  <div class="highlight"><pre class="chroma"><code class="language-shell" data-lang="shell">rudder-cli auth login</code></pre></div>
</div>
<!-- end-chunk -->
<!-- begin-chunk -->
<h2 id="2-create-a-project-directory">2. Create a project directory</h2><p>Create a project directory to store your transformation specs, code, and tests:</p>
<div class="rs-code">
  <div class="rs-code__head">shell<button class="rs-code__copy" type="button">
      
      Copy
    </button>
  </div>
  <div class="highlight"><pre class="chroma"><code class="language-shell" data-lang="shell">mkdir -p ~/tutorial-transformations/transformations</code></pre></div>
</div>
<p>Example structure:</p>
<div class="rs-code">
  <div class="rs-code__head"><button class="rs-code__copy" type="button">
      
      Copy
    </button>
  </div>
  <div class="highlight"><pre class="chroma"><code class="language-text" data-lang="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</code></pre></div>
</div>

<blockquote class="announcement">
  <div class="tip-quote">
    
    <div class="tip-text">RudderStack supports Python transformation libraries only in the <a href="https://rudderstack.com/pricing/" >Growth</a> and <a href="https://www.rudderstack.com/enterprise-quote/" >Enterprise</a> plans.</div>
  </div>
</blockquote>

<!-- end-chunk -->
<!-- begin-chunk -->
<h2 id="3-add-a-transformation-spec">3. Add a transformation spec</h2><p>This section lists the steps to create a transformation YAML spec via the following methods:</p>
<ul>
<li><a href="#referencing-an-external-file" >Referencing an external file</a></li>
<li><a href="#using-inline-code" >Using inline code</a></li>
</ul>
<!-- end-chunk -->
<!-- begin-chunk -->
<h4 id="referencing-an-external-file">Referencing an external file</h4><ol>
<li>Create the YAML spec for your transformation:</li>
</ol>
<div class="rs-tabs">
	<div class="rs-tabs__list">
		<button type="button" class="rs-tabs__tab"
			id="tab-dceafb"
		
		>JavaScript</button>
		<button type="button" class="rs-tabs__tab"
			id="tab-bcdaef"
		
		>Python</button>
	</div>
	
<div class="rs-tabs__panel" id="panel-dceafb">

<p><code>~/tutorial-transformations/transformations/my-transformation.yaml</code>:</p>
<div class="rs-code">
  <div class="rs-code__head">yaml<button class="rs-code__copy" type="button">
      
      Copy
    </button>
  </div>
  <div class="highlight"><pre class="chroma"><code class="language-yaml" data-lang="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</code></pre></div>
</div>

</div>

<div class="rs-tabs__panel" id="panel-bcdaef" hidden>

<p><code>~/tutorial-transformations/transformations/my-python-transformation.yaml</code>:</p>
<div class="rs-code">
  <div class="rs-code__head">yaml<button class="rs-code__copy" type="button">
      
      Copy
    </button>
  </div>
  <div class="highlight"><pre class="chroma"><code class="language-yaml" data-lang="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</code></pre></div>
</div>

</div>

</div>

<ol start="2">
<li>Create the transformation code file:</li>
</ol>
<div class="rs-tabs">
	<div class="rs-tabs__list">
		<button type="button" class="rs-tabs__tab"
			id="tab-beadcf"
		
		>JavaScript</button>
		<button type="button" class="rs-tabs__tab"
			id="tab-bcfade"
		
		>Python</button>
	</div>
	
<div class="rs-tabs__panel" id="panel-beadcf">

<p><code>~/tutorial-transformations/transformations/javascript/my-transformation.js</code>:</p>
<div class="rs-code">
  <div class="rs-code__head">javascript<button class="rs-code__copy" type="button">
      
      Copy
    </button>
  </div>
  <div class="highlight"><pre class="chroma"><code class="language-javascript" data-lang="javascript">export function transformEvent(event, metadata) {
  event.context = event.context || {};
  event.context.cliManaged = true;
  return event;
}</code></pre></div>
</div>

</div>

<div class="rs-tabs__panel" id="panel-bcfade" hidden>

<p><code>~/tutorial-transformations/transformations/python/my-python-transformation.py</code>:</p>
<div class="rs-code">
  <div class="rs-code__head">python<button class="rs-code__copy" type="button">
      
      Copy
    </button>
  </div>
  <div class="highlight"><pre class="chroma"><code class="language-python" data-lang="python">from datetime import datetime, timezone

def transformEvent(event, metadata):
    event.setdefault(&#34;context&#34;, {})
    event[&#34;context&#34;][&#34;processedAt&#34;] = datetime.now(timezone.utc).isoformat()
    return event</code></pre></div>
</div>

</div>

</div>

<!-- end-chunk -->
<!-- begin-chunk -->
<h4 id="using-inline-code">Using inline code</h4><p>You can also write transformation code directly in the YAML spec using <code>code</code> instead of referencing an external file with <code>file</code>:</p>
<div class="rs-tabs">
	<div class="rs-tabs__list">
		<button type="button" class="rs-tabs__tab"
			id="tab-efadbc"
		
		>JavaScript</button>
		<button type="button" class="rs-tabs__tab"
			id="tab-afecdb"
		
		>Python</button>
	</div>
	
<div class="rs-tabs__panel" id="panel-efadbc">

<div class="rs-code">
  <div class="rs-code__head">yaml<button class="rs-code__copy" type="button">
      
      Copy
    </button>
  </div>
  <div class="highlight"><pre class="chroma"><code class="language-yaml" data-lang="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 = &#34;inline-transformation&#34;;
      return event;
    }    </code></pre></div>
</div>

</div>

<div class="rs-tabs__panel" id="panel-afecdb" hidden>

<div class="rs-code">
  <div class="rs-code__head">yaml<button class="rs-code__copy" type="button">
      
      Copy
    </button>
  </div>
  <div class="highlight"><pre class="chroma"><code class="language-yaml" data-lang="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(&#34;context&#34;, {})
        event[&#34;context&#34;][&#34;processedBy&#34;] = &#34;inline-python-transformation&#34;
        return event    </code></pre></div>
</div>

</div>

</div>

<html lang="en">
<blockquote class="info">
  <div class="tip-quote">
    
    <div class="tip-text"><code>code</code> and <code>file</code> are mutually exclusive — use one or the other in a given spec. See the <a href="https://www.rudderstack.com/docs/dev-tools/rudder-cli/yaml-transformations/" >Transformation YAML Reference</a> for the complete schema.</div>
  </div>
</blockquote>

</html>
<!-- end-chunk -->
<!-- begin-chunk -->
<h2 id="4-add-a-transformation-library-spec">4. Add a transformation library spec</h2><p>Create the YAML spec for your transformation library:</p>
<div class="rs-tabs">
	<div class="rs-tabs__list">
		<button type="button" class="rs-tabs__tab"
			id="tab-acfdbe"
		
		>JavaScript</button>
		<button type="button" class="rs-tabs__tab"
			id="tab-fbdcea"
		
		>Python</button>
	</div>
	
<div class="rs-tabs__panel" id="panel-acfdbe">

<p><code>~/tutorial-transformations/transformations/my-library.yaml</code>:</p>
<div class="rs-code">
  <div class="rs-code__head">yaml<button class="rs-code__copy" type="button">
      
      Copy
    </button>
  </div>
  <div class="highlight"><pre class="chroma"><code class="language-yaml" data-lang="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</code></pre></div>
</div>

</div>

<div class="rs-tabs__panel" id="panel-fbdcea" hidden>

<p><code>~/tutorial-transformations/transformations/my-python-library.yaml</code>:</p>
<div class="rs-code">
  <div class="rs-code__head">yaml<button class="rs-code__copy" type="button">
      
      Copy
    </button>
  </div>
  <div class="highlight"><pre class="chroma"><code class="language-yaml" data-lang="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</code></pre></div>
</div>

</div>

</div>

<p>Then, create the library code file:</p>
<div class="rs-tabs">
	<div class="rs-tabs__list">
		<button type="button" class="rs-tabs__tab"
			id="tab-fabced"
		
		>JavaScript</button>
		<button type="button" class="rs-tabs__tab"
			id="tab-abfdce"
		
		>Python</button>
	</div>
	
<div class="rs-tabs__panel" id="panel-fabced">

<p><code>~/tutorial-transformations/transformations/javascript/my-library.js</code>:</p>
<div class="rs-code">
  <div class="rs-code__head">javascript<button class="rs-code__copy" type="button">
      
      Copy
    </button>
  </div>
  <div class="highlight"><pre class="chroma"><code class="language-javascript" data-lang="javascript">export function addTag(event, key, value) {
  event.context = event.context || {};
  event.context[key] = value;
  return event;
}</code></pre></div>
</div>

</div>

<div class="rs-tabs__panel" id="panel-abfdce" hidden>

<p><code>~/tutorial-transformations/transformations/python/my-python-library.py</code>:</p>
<div class="rs-code">
  <div class="rs-code__head">python<button class="rs-code__copy" type="button">
      
      Copy
    </button>
  </div>
  <div class="highlight"><pre class="chroma"><code class="language-python" data-lang="python">def add_tag(event, key, value):
    event.setdefault(&#34;context&#34;, {})
    event[&#34;context&#34;][key] = value
    return event</code></pre></div>
</div>

</div>

</div>

<html lang="en">
<blockquote class="info">
  <div class="tip-quote">
    
    <div class="tip-text">For library resources, <code>import_name</code> must be the camelCase form of <code>name</code>. For example, <code>name: my library</code> maps to <code>import_name: myLibrary</code> and <code>name: my python library</code> maps to <code>import_name: myPythonLibrary</code>.</div>
  </div>
</blockquote>

</html>
<!-- end-chunk -->
<!-- begin-chunk -->
<h2 id="5-validate-yaml-and-code-references">5. Validate YAML and code references</h2><p>Run validation from the project root:</p>
<div class="rs-code">
  <div class="rs-code__head">shell<button class="rs-code__copy" type="button">
      
      Copy
    </button>
  </div>
  <div class="highlight"><pre class="chroma"><code class="language-shell" data-lang="shell">rudder-cli validate -l ~/tutorial-transformations</code></pre></div>
</div>
<p>This command validates required fields, <code>code</code>/<code>file</code> rules, supported languages, code syntax, and test definitions.</p>
<!-- end-chunk -->
<!-- begin-chunk -->
<h2 id="6-test-transformations-locally">6. Test transformations locally</h2><p>Rudder CLI supports 3 test modes:</p>

<blockquote class="warning">
  <div class="tip-quote">
    
    <div class="tip-text">You cannot include multiple test modes in a single command.</div>
  </div>
</blockquote>
<ol>
<li>Test a specific transformation by ID:</li>
</ol>
<div class="rs-code">
  <div class="rs-code__head">shell<button class="rs-code__copy" type="button">
      
      Copy
    </button>
  </div>
  <div class="highlight"><pre class="chroma"><code class="language-shell" data-lang="shell">rudder-cli transformations test my-transformation -l ~/tutorial-transformations</code></pre></div>
</div>
<ol start="2">
<li>Test all transformations:</li>
</ol>
<div class="rs-code">
  <div class="rs-code__head">shell<button class="rs-code__copy" type="button">
      
      Copy
    </button>
  </div>
  <div class="highlight"><pre class="chroma"><code class="language-shell" data-lang="shell">rudder-cli transformations test --all -l ~/tutorial-transformations</code></pre></div>
</div>
<ol start="3">
<li>Test only new or modified transformations:</li>
</ol>
<div class="rs-code">
  <div class="rs-code__head">shell<button class="rs-code__copy" type="button">
      
      Copy
    </button>
  </div>
  <div class="highlight"><pre class="chroma"><code class="language-shell" data-lang="shell">rudder-cli transformations test --modified -l ~/tutorial-transformations</code></pre></div>
</div>
<p><strong>Optional</strong></p>
<ul>
<li>Show detailed failures and diffs:</li>
</ul>
<div class="rs-code">
  <div class="rs-code__head">shell<button class="rs-code__copy" type="button">
      
      Copy
    </button>
  </div>
  <div class="highlight"><pre class="chroma"><code class="language-shell" data-lang="shell">rudder-cli transformations test --all --verbose -l ~/tutorial-transformations</code></pre></div>
</div>
<ul>
<li>Show the built-in default events:</li>
</ul>
<div class="rs-code">
  <div class="rs-code__head">shell<button class="rs-code__copy" type="button">
      
      Copy
    </button>
  </div>
  <div class="highlight"><pre class="chroma"><code class="language-shell" data-lang="shell">rudder-cli transformations show-default-events</code></pre></div>
</div>
<!-- end-chunk -->
<!-- begin-chunk -->
<h4 id="how-it-works">How it works</h4><p>When running tests, Rudder CLI loads input files from each test suite&rsquo;s <code>input</code> path. If a matching output file (same filename) exists in the <code>output</code> 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.</p>

<html lang="en">
<blockquote class="info">
  <div class="tip-quote">
    
    <div class="tip-text"><p>Rudder CLI tests the transformation using default RudderStack events if:</p>
<ul>
<li><code>spec.tests</code> isn&rsquo;t configured, or</li>
<li>No input JSON files are found in the configured input paths</li>
</ul></div>
  </div>
</blockquote>

</html>
<p>See the <a href="https://www.rudderstack.com/docs/dev-tools/rudder-cli/yaml-transformations/#testing-framework" >Rudder CLI transformations testing framework</a> section for more details.</p>
<!-- end-chunk -->
<!-- begin-chunk -->
<h2 id="7-apply-changes">7. Apply changes</h2><ol>
<li>Optional dry-run:</li>
</ol>
<div class="rs-code">
  <div class="rs-code__head">shell<button class="rs-code__copy" type="button">
      
      Copy
    </button>
  </div>
  <div class="highlight"><pre class="chroma"><code class="language-shell" data-lang="shell">rudder-cli apply -l ~/tutorial-transformations --dry-run</code></pre></div>
</div>
<ol start="2">
<li>Apply to workspace:</li>
</ol>
<div class="rs-code">
  <div class="rs-code__head">shell<button class="rs-code__copy" type="button">
      
      Copy
    </button>
  </div>
  <div class="highlight"><pre class="chroma"><code class="language-shell" data-lang="shell">rudder-cli apply -l ~/tutorial-transformations</code></pre></div>
</div>
<!-- end-chunk -->
<!-- begin-chunk -->
<h2 id="next-steps">Next steps</h2><ul>
<li>See the <a href="https://www.rudderstack.com/docs/dev-tools/rudder-cli/yaml-transformations/" >Transformation YAML Reference</a> for detailed resource schemas and constraints</li>
<li>See <a href="https://www.rudderstack.com/docs/dev-tools/rudder-cli/manage-transformations/" >Manage Transformations Using Rudder CLI</a> for the overall CLI workflow</li>
<li>Automate with <a href="https://www.rudderstack.com/docs/dev-tools/rudder-cli/github-actions/transformations/" >GitHub Actions</a></li>
</ul>

