diff --git a/travis/4-custom-transformers.md b/travis/4-custom-transformers.md
new file mode 100644
index 0000000..10510a4
--- /dev/null
+++ b/travis/4-custom-transformers.md
@@ -0,0 +1,248 @@
+# Using custom transformers to customize Valet's behavior
+
+In this lab you will build upon the `dry-run` command to override Valet's default behavior and customize the converted workflow using "custom transformers". Custom transformers can be used to:
+
+1. Convert items that are not automatically converted.
+2. Convert items that were automatically converted using different actions.
+3. Convert environment variable values differently.
+4. Convert references to runners to use a different runner name in Actions.
+
+## Prerequisites
+
+1. Followed the steps [here](./readme.md#configure-your-codespace) to set up your GitHub Codespaces environment.
+2. Completed the [configure lab](./1-configure.md#configuring-credentials).
+3. Completed the [dry-run lab](./3-dry-run.md).
+
+## Perform a dry-run
+
+You will be performing a `dry-run` command to inspect the workflow that is converted by default. Run the following command within the codespace terminal:
+
+```bash
+ gh valet dry-run travis-ci --travis-ci-repository "deploy-example" --output-dir tmp/travis/dry-run
+```
+
+The converted workflow that is generated by the above command can be seen below:
+
+
+ Converted workflow 👇
+
+```yaml
+ name: valet-travis-labs/deploy-example
+ on:
+ push:
+ branches:
+ - "**/*"
+ pull_request:
+ concurrency:
+ # # This item has no matching transformer
+ # maximum_number_of_builds: 0
+ env:
+ DB: mysql
+ jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - name: checkout
+ uses: actions/checkout@v2
+ - run: gem install bundler
+ - run: bundle install --jobs=3 --retry=3
+ - run: rake
+ - run: echo 'ready?'
+ # # This item has no matching transformer
+ # - codedeploy:
+ # provider: codedeploy
+ # access_key_id: YOUR AWS ACCESS KEY
+ # secret_access_key: YOUR AWS SECRET KEY
+ # bucket: S3 Bucket
+ # key: latest/MyApp.zip
+ # application: MyApp
+ # deployment_group: MyDeploymentGroup
+ # if: "${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}"
+ - run: "./after_deploy_1.sh"
+ - run: "./after_deploy_2.sh"
+ - uses: desiderati/github-action-pushover@v1
+ with:
+ job-status: "${{ job.status }}"
+ pushover-api-token: '12345'
+ pushover-user-key: "${{ secrets.PUSHOVER_USER_KEY }}"
+ if: "${{ github.event_name != 'pull_request' }}"
+
+```
+
+
+
+_Note_: You can refer to the previous [lab](./3-dry-run.md) to learn about the fundamentals of the `dry-run` command.
+
+## Custom transformers for an unknown step
+
+The converted workflow above contains an `codedeploy` step that was not automatically converted. Answer the following questions before writing a custom transformer:
+
+1. What is the "identifier" of the step to customize?
+ - __codedeploy__
+
+2. What is the desired Actions syntax to use instead?
+ - After some research, you have determined that we can login to AWS with the `aws-actions/configure-aws-credentials@v1` action, and deploy the app to AWS using a run step to replace the functionality of `codedeploy`:
+
+ ```yaml
+ - uses: aws-actions/configure-aws-credentials@v1
+ with:
+ role-to-assume: owner
+ role-session-name: GitHub-Action-Role
+ aws-region: east-2
+ - run: |
+ echo "Deploying branch ${{ env.GITHUB_REF }} to ${{ github.event.inputs.environment }}"
+ commit_hash=`git rev-parse HEAD`
+ aws deploy create-deployment --application-name MyApp --deployment-group-name MyDeploymentGroup --github-location repository=$GITHUB_REPOSITORY,commitId=$commit_hash --ignore-application-stop-failures
+ ```
+
+Now you can begin to write the custom transformer. Custom transformers use a DSL built on top of Ruby and should be defined in a file with the `.rb` file extension. You can create this file by running the following command in your codespace terminal:
+
+```bash
+code transformers.rb
+```
+
+Next, you will define a `transform` method for the `codedeploy` identifier by adding the following code to `transformers.rb`:
+
+```ruby
+transform "codedeploy" do |_item|
+ [
+ {
+ "uses": "aws-actions/configure-aws-credentials@v1",
+ "with": {
+ "role-to-assume": "owner",
+ "role-session-name": "GitHub-Action-Role",
+ "aws-region": "east-2"
+ }
+ },
+ {
+ "run": "echo \"Deploying branch ${{ env.GITHUB_REF }} to ${{ github.event.inputs.environment }}\"\ncommit_hash=`git rev-parse HEAD`\naws deploy create-deployment --application-name MyApp --deployment-group-name MyDeploymentGroup --github-location repository=$GITHUB_REPOSITORY,commitId=$commit_hash --ignore-application-stop-failures\n"
+ }
+ ]
+end
+```
+
+This method can use any valid ruby syntax and should return a `Hash`, or an array of `Hashes` that represent the YAML that should be generated for a given step. Valet will use this method to convert a step with the provided identifier and will use the `item` parameter for the original values configured in Travis CI.
+
+Now you can perform another `dry-run` command and use the `--custom-transformers` CLI option to provide this custom transformer. Run the following command within your codespace terminal:
+
+```bash
+ gh valet dry-run travis-ci --travis-ci-repository "deploy-example" --output-dir tmp/travis/dry-run --custom-transformers transformers.rb
+```
+
+The converted workflow that is generated by the above command will now use the custom logic for the `codedeploy` step.
+
+```diff
+- # # This item has no matching transformer
+- # - codedeploy:
+- # provider: codedeploy
+- # access_key_id: YOUR AWS ACCESS KEY
+- # secret_access_key: YOUR AWS SECRET KEY
+- # bucket: S3 Bucket
+- # key: latest/MyApp.zip
+- # application: MyApp
+- # deployment_group: MyDeploymentGroup
+- # if: "${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}"
++ - uses: aws-actions/configure-aws-credentials@v1
++ with:
++ role-to-assume: owner
++ role-session-name: GitHub-Action-Role
++ aws-region: east-2
++ - run: |
++ echo "Deploying branch ${{ env.GITHUB_REF }} to ${{ github.event.inputs.environment }}"
++ commit_hash=`git rev-parse HEAD`
++ aws deploy create-deployment --application-name MyApp --deployment-group-name MyDeploymentGroup --github-location repository=$GITHUB_REPOSITORY,commitId=$commit_hash --ignore-application-stop-failures
+```
+
+_Note_: We hard coded certain values such as the `application-name`, but we can apply these properties programmatically as well by using the item passed into the transform method. If you were unsure what the data structure of `item` was, you could use the following code in the custom transformer to print `item` to the console:
+
+```ruby
+transform "codecov_codecov_upload" do |item|
+ puts item
+end
+```
+
+## Custom transformers for environment variables
+
+You can use custom transformers to edit the values of environment variables in converted workflows. In this example, you will update the `DB` environment variable to be `sqlite` instead of `mysql`.
+
+To do this, add the following code to the `transformers.rb` file.
+
+```ruby
+env "DB", "sqlite"
+```
+
+In this example, the first parameter to the `env` method is the environment variable name and the second is the updated value.
+
+Now you can perform another `dry-run` command with the `--custom-transformers` CLI option. When you open the converted workflow the `DB` environment variable will be set to `sqlite`:
+
+```diff
+ env:
+- DB: "mysql"
++ DB: "sqlite"
+```
+
+## Custom transformers for runners
+
+Finally, you can use custom transformers to dictate which runners converted workflows should use. First, answer the following questions:
+
+1. What is the label of the runner in Travis to update?
+ - __linux__
+
+2. What is the label of the runner in Actions to use instead?
+ - __new-runner__
+
+With these questions answered, you can add the following code to the `transformers.rb` file:
+
+```ruby
+runner "linux", ["new-runner", "self-hosted"]
+```
+
+In this example, the first parameter to the `runner` method is the Azure DevOps label and the second is the Actions runner labels.
+
+Now you can perform another `dry-run` command with the `--custom-transformers` CLI option. When you open the converted workflow, the `runs-on` statement will use the customized runner labels:
+
+```diff
+- runs-on: ubuntu-latest
++ runs-on:
++ - new-runner
++ - self-hosted
+```
+
+At this point, the file contents of `transformers.rb` should match this:
+
+
+ Custom transformers 👇
+
+```ruby
+transform "codedeploy" do |_item|
+ [
+ {
+ "uses": "aws-actions/configure-aws-credentials@v1",
+ "with": {
+ "role-to-assume": "owner",
+ "role-session-name": "GitHub-Action-Role",
+ "aws-region": "east-2"
+ }
+ },
+ {
+ "run": "echo \"Deploying branch ${{ env.GITHUB_REF }} to ${{ github.event.inputs.environment }}\"\ncommit_hash=`git rev-parse HEAD`\naws deploy create-deployment --application-name MyApp --deployment-group-name MyDeploymentGroup --github-location repository=$GITHUB_REPOSITORY,commitId=$commit_hash --ignore-application-stop-failures\n"
+ }
+ ]
+end
+
+env "DB", "sqlite"
+
+runner "linux", ["new-runner", "self-hosted"]
+```
+
+
+
+That's it! Congratulations, you have overridden Valet's default behavior by customizing the conversion of:
+
+- Unknown steps
+- Environment variables
+- Runner
+
+## Next lab
+
+[Perform a production migration of a Travis CI pipeline](5-migrate.md)