The Scotty.sh format | scotty | Spatie

 SPATIE

  Scotty
=========

spatie.be/open-source

  [Docs](https://spatie.be/docs)  [Scotty](https://spatie.be/docs/scotty/v1)  Basic-usage  The Scotty.sh format

 Version   v1

 Other versions for crawler [v1](https://spatie.be/docs/scotty/v1)

  The Scotty.sh format
- [ Introduction ](https://spatie.be/docs/scotty/v1/introduction)
- [ Support us ](https://spatie.be/docs/scotty/v1/support-us)
- [ Getting started ](https://spatie.be/docs/scotty/v1/getting-started)
- [ Requirements ](https://spatie.be/docs/scotty/v1/requirements)
- [ Installation &amp; setup ](https://spatie.be/docs/scotty/v1/installation)
- [ Questions and issues ](https://spatie.be/docs/scotty/v1/questions-issues)
- [ Changelog ](https://spatie.be/docs/scotty/v1/changelog)
- [ About us ](https://spatie.be/docs/scotty/v1/about-us)

Basic Usage
-----------

- [ Your first deploy script ](https://spatie.be/docs/scotty/v1/basic-usage/your-first-deploy-script)
- [ The Scotty.sh format ](https://spatie.be/docs/scotty/v1/basic-usage/bash-format)
- [ Running tasks ](https://spatie.be/docs/scotty/v1/basic-usage/running-tasks)

Advanced Usage
--------------

- [ Doctor ](https://spatie.be/docs/scotty/v1/advanced-usage/doctor)
- [ File lookup order ](https://spatie.be/docs/scotty/v1/advanced-usage/file-lookup-order)
- [ Envoy compatibility ](https://spatie.be/docs/scotty/v1/advanced-usage/envoy-compatibility)
- [ Zero-downtime deployments ](https://spatie.be/docs/scotty/v1/advanced-usage/zero-downtime-deployments)

 The Scotty.sh format
====================

###  On this page

1. [ Servers ](#content-servers)
2. [ Tasks ](#content-tasks)
3. [ Macros ](#content-macros)
4. [ Variables ](#content-variables)
5. [ Helper functions ](#content-helper-functions)
6. [ Hooks ](#content-hooks)
7. [ Complete example ](#content-complete-example)
8. [ Migrating from Envoy ](#content-migrating-from-envoy)

The `Scotty.sh` format is plain bash with annotation comments. Every line is real bash, so your editor highlights it correctly and all your existing shell tooling works.

Servers
-----------------------------------------------------------------------------

At the top of your file, define which servers you want to connect to:

```
# @servers local=127.0.0.1 remote=deployer@your-server.com
```

You can define as many as you need:

```
# @servers local=127.0.0.1 web-1=deployer@1.1.1.1 web-2=deployer@2.2.2.2
```

If your server listens on a non-default SSH port, append it to the host with a colon:

```
# @servers remote=deployer@your-server.com:2222
```

For more complex SSH options (identity files, jump hosts, ProxyCommand, etc.), put them in `~/.ssh/config` under a `Host` block and reference that host name from `@servers`.

Tasks
-----------------------------------------------------------------------

A task is just a bash function with a `# @task` annotation above it. The `on:` parameter tells Scotty which server to run it on:

```
# @task on:remote
deploy() {
    cd /var/www/my-app
    git pull origin main
    php artisan migrate --force
}
```

That's the core concept. Everything else builds on this.

### Running on multiple servers

You can target multiple servers by separating their names with commas:

```
# @task on:web-1,web-2
deploy() {
    cd /var/www/my-app
    git pull origin main
}
```

By default, the task runs on each server one after the other.

### Parallel execution

If you want to run on all servers at the same time, add `parallel`:

```
# @task on:web-1,web-2 parallel
restartWorkers() {
    sudo supervisorctl restart all
}
```

This is handy for things like restarting workers across a cluster, where you don't need to wait for one to finish before starting the next.

### Confirmation

For dangerous tasks (like deploying to production), you can require confirmation:

```
# @task on:remote confirm="Are you sure you want to deploy to production?"
deploy() {
    cd /var/www/my-app
    git pull origin main
}
```

Scotty will ask before running the task. If you say no, it stops.

Macros
--------------------------------------------------------------------------

A macro groups multiple tasks together so you can run them with a single command:

```
# @macro deploy pullCode runComposer clearCache restartWorkers
```

If the list gets long, you can use the multi-line format:

```
# @macro deploy
#   pullCode
#   runComposer
#   generateAssets
#   updateSymlinks
#   clearCache
#   restartWorkers
# @endmacro
```

Run it with `scotty run deploy`. The tasks execute in the order you listed them. If any task fails, execution stops immediately.

Variables
-----------------------------------------------------------------------------------

You can define variables at the top of your file, right after the server and macro lines:

```
BRANCH="main"
REPOSITORY="your/repo"
APP_DIR="/var/www/my-app"
RELEASES_DIR="$APP_DIR/releases"
NEW_RELEASE_NAME=$(date +%Y%m%d-%H%M%S)
```

These are plain bash variables, so computed values like `$(date)` work naturally. All variables are available in all tasks.

Top-level variable assignments are evaluated **once locally** before the first task runs. The captured values are then injected into every task's script. This means a value like `NEW_RELEASE_NAME=$(date +%Y%m%d-%H%M%S)` produces the same timestamp in every task of the same run, which is what you want for zero-downtime deploys where multiple tasks need to agree on a release directory.

Helper functions defined at the top of the file still run inside each task on the remote server. Side effects in top-level assignments (`mkdir`, `rm`, etc.) happen on your local machine, not on the remote. If you need work to happen remotely, put it in a task.

You can also accept variables from the command line by declaring them with `# @option`. Three forms are supported:

```
# @option staging          # boolean flag — $STAGING='1' when --staging is passed
# @option branch=main      # value with default — $BRANCH='main' unless overridden
# @option release-name=    # required value — scotty errors if --release-name=... is missing
```

```
scotty run deploy --branch=develop --release-name=v42 --staging
```

The key gets uppercased and dashes become underscores, so `--release-name=v42` sets `$RELEASE_NAME`. Value options also fall back to an environment variable of the same (uppercased) name before using the declared default. See [Dynamic options](/docs/scotty/v1/basic-usage/running-tasks#dynamic-options) for the full precedence rules.

Helper functions
--------------------------------------------------------------------------------------------------------

Any function without a `# @task` annotation is treated as a helper. Helpers are available in all tasks:

```
log() {
    echo -e "\033[32m$1\033[0m"
}

# @task on:remote
deploy() {
    log "Deploying..."
    cd /var/www/my-app
    git pull origin main
}
```

Hooks
-----------------------------------------------------------------------

You can run code at different points during execution. This is useful for things like sending Slack notifications:

```
# @before
beforeEachTask() {
    echo "Starting task..."
}

# @after
afterEachTask() {
    echo "Task done."
}

# @success
onSuccess() {
    curl -X POST https://hooks.slack.com/... \
        -d '{"text": "Deploy succeeded!"}'
}

# @error
onError() {
    curl -X POST https://hooks.slack.com/... \
        -d '{"text": "Deploy failed!"}'
}

# @finished
onFinished() {
    echo "Deploy process complete."
}
```

`@before` and `@after` run around each task. `@success` and `@error` run once at the end depending on whether everything passed. `@finished` always runs, regardless of the outcome.

Complete example
--------------------------------------------------------------------------------------------------------

Here's a full deploy script using all the concepts above:

```
#!/usr/bin/env scotty

# @servers local=127.0.0.1 remote=deployer@your-server.com
# @macro deploy
#   startDeployment
#   cloneRepository
#   runComposer
#   blessNewRelease
#   cleanOldReleases
# @endmacro

BRANCH="main"
REPOSITORY="your/repo"
APP_DIR="/var/www/my-app"
RELEASES_DIR="$APP_DIR/releases"
CURRENT_DIR="$APP_DIR/current"
NEW_RELEASE_NAME=$(date +%Y%m%d-%H%M%S)
NEW_RELEASE_DIR="$RELEASES_DIR/$NEW_RELEASE_NAME"

# @task on:local
startDeployment() {
    git checkout $BRANCH
    git pull origin $BRANCH
}

# @task on:remote
cloneRepository() {
    cd $RELEASES_DIR
    git clone --depth 1 git@github.com:$REPOSITORY --branch $BRANCH $NEW_RELEASE_NAME
}

# @task on:remote
runComposer() {
    cd $NEW_RELEASE_DIR
    ln -nfs $APP_DIR/.env .env
    composer install --prefer-dist --no-dev -o
}

# @task on:remote
blessNewRelease() {
    ln -nfs $NEW_RELEASE_DIR $CURRENT_DIR
    sudo service php8.4-fpm restart
}

# @task on:remote
cleanOldReleases() {
    cd $RELEASES_DIR
    ls -dt $RELEASES_DIR/* | tail -n +4 | xargs rm -rf
}
```

Migrating from Envoy
--------------------------------------------------------------------------------------------------------------------

If you're coming from Laravel Envoy, here's a quick reference. For the full Blade format documentation, see the [Envoy compatibility](/docs/scotty/v1/advanced-usage/envoy-compatibility) page.

Blade formatScotty.sh format`@servers(['remote' => '1.1.1.1'])``# @servers remote=1.1.1.1``@task('deploy', ['on' => 'remote'])``# @task on:remote``@endtask``}` (end of function)`@story('deploy')` ... `@endstory``# @macro deploy task1 task2``{{ $variable }}``$VARIABLE``@setup` PHP blockShell `$(command)` substitution`@if($condition)`Bash `if [ condition ]`

 A good
match?
-------------

### What we do best

- All things Laravel
- Custom frontend components
- Building APIs
- AI-powered features
- Simplifying things
- Clean solutions
- Integrating services

### Not our cup of tea

- WordPress themes
- Cutting corners
- Free mockups to win a job
- "Just execute the briefing"

 In short: we'd like to be a **substantial part** of your project.

 [ Get in touch via email ](mailto:info@spatie.be?subject=A%20good%20match%21&body=Tell%20us%20as%20much%20as%20you%20can%20about%0A-%20your%20online%20project%0A-%20your%20planning%0A-%20your%20budget%0A-%20%E2%80%A6%0A%0AAnything%20that%20helps%20us%20to%20start%20straightforward%21)
