# The code workflow

**Goal:** know the full code workflow on the Acquia side: where code lives, how it reaches production, how data moves between [environments](/start-here/glossary/#environment), and how to run [Drush](/start-here/glossary/#drush) against them.

The workflow is the same whether your site renders [in-platform](/start-here/glossary/#in-platform-rendering) or serves a [headless](/start-here/glossary/#headless) frontend. For writing the Drupal application itself (modules, hooks, theming), see [drupal.org/docs](https://www.drupal.org/docs); for configuring or administering the subscription, see [docs.acquia.com](https://docs.acquia.com).

## What you'll have when you're done

- A repository laid out the way Cloud Platform expects, and the reason `docroot/` is non-negotiable.
- A working model of code flow: branches on dev, tags on prod, push-to-deploy in between.
- Databases and files moving between environments in both directions.
- Drush running against any environment from your terminal.

## Prerequisites

- The deploy loop from the [code-workflow quickstart](/cloud-platform/code-workflow/quickstart/) (you've pushed at least once)
- [acli](/start-here/glossary/#acli) authenticated ([CLI quickstart](/cloud-platform/cli/quickstart/))

## Steps

<Steps>

1. ### Lay the repository out the way Cloud Platform expects

   ```text
   myapp/
   ├── composer.json      # Drupal + dependencies (Composer)
   ├── docroot/           # the webroot Cloud Platform uses
   ├── config/            # exported Drupal configuration
   └── hooks/             # Cloud Hooks: run on deploy events
   ```

   `docroot/` is the fixed contract: Cloud Platform serves from it, so Drupal's `index.php` must live there (projects using `web/` rename or symlink; set Composer's installer paths to `docroot/` from the start). `hooks/` is optional but is where post-deploy automation like cache rebuilds belongs.

   For a repository with no codebase in it yet, [Set up a codebase](/cloud-platform/code-workflow/set-up-a-codebase/) scaffolds one in exactly this layout and pushes it.

2. ### Move code forward: branches to Dev and Stage, tags to Prod

   Each environment tracks one branch or tag, shown on its environment card. The card shows the environment's label, while commands, aliases, and `AH_SITE_ENVIRONMENT` take its name. The working convention, label and name together:

   - **Dev** (`dev`) tracks a branch: pushing to it deploys it (the quickstart's loop).
   - **Stage** (`test`) tracks a branch you promote into when dev looks right.
   - **Prod** (`prod`) tracks a **tag**. Cut one and switch to it, so production state is immutable and rollback is "switch back to the previous tag":

   ```bash
   git tag 2026-07-02
   git push origin 2026-07-02
   ```

   Those two commands run in a clone of the application's Acquia repository (the [quickstart](/cloud-platform/code-workflow/quickstart/)'s checkout), where the deployed branch already holds built code. On the artifact flow from [set up a codebase](/cloud-platform/code-workflow/set-up-a-codebase/), your source repository's commits never reach the platform, so a tag on them deploys nothing. Cut the tag from a build instead: `acli push:artifact --destination-git-branch=<deployed-branch> --destination-git-tag=2026-07-02` builds the artifact on the named artifact branch and pushes it as that tag (see [push:artifact](/cloud-platform/reference/acli/#pushartifact)).

   Switching an environment onto that tag is one command:

   ```bash
   acli api:environments:code-switch myapp.prod "tags/2026-07-02" --task-wait
   ```

   **A tag has to be prefixed with `tags/`**, which is the one thing about this command that is easy to get wrong; a branch name is passed bare (`acli api:environments:code-switch myapp.dev my-feature-branch`). `--task-wait` holds the terminal until the deploy task settles and exits non-zero if it failed, which is what makes the command usable in a script. `Switch code` on the environment card in the Cloud Platform user interface does the same thing.

   On production, switching code is a destructive-grade permission either way: restrict who holds it through Cloud Platform roles, which separate pulling and deploying code on production from the same operations on non-production (see [About Cloud Platform permissions](https://docs.acquia.com/acquia-cloud-platform/about-cloud-platform-permissions) on docs.acquia.com).

3. ### Move data backward: databases and files flow down, not up

   Code flows dev → prod; data flows prod → dev. Copy a database or the files directory between environments with `acli api:environments:database-copy myapp.dev my_db <source-env-id>` and `acli api:environments:file-copy myapp.dev --source=<source-env-id>`; both queue a task that `--task-wait` blocks on. To bring production data to your machine instead:

   ```bash
   acli pull:db myapp.prod
   acli pull:files myapp.prod
   ```

   Copying a database **overwrites the target**: copying prod to dev destroys whatever dev's database held. That's the routine direction precisely because dev data is disposable; never copy dev over prod. Like production `Switch code`, database copies are role-gated: grant them deliberately, because one misdirected copy is a restore-from-backup incident.

   The two directions run opposite each other, and a deploy fires the Cloud Hooks covered below:

   <ConceptDiagram name="code-promotion" height={380} />

4. ### Run Drush against an environment

   `acli` proxies Drush to any environment, no SSH setup beyond your registered key:

   ```bash
   acli remote:drush myapp.dev -- status
   acli remote:drush myapp.dev -- cr
   ```

   Environments that harden PHP by disabling process execution reject `remote:drush` with `pcntl_exec(): ... Permission denied` (the Drush launcher can't hand off to the site's Drush). On those, run the site's `drush.php` through PHP over SSH instead, the same invocation the Cloud Hooks below use:

   ```bash
   acli remote:ssh myapp.dev -- \
     'php /var/www/html/vendor/drush/drush/drush.php \
      --root=/var/www/html/docroot status'
   ```

   For anything more interactive, open a shell on the environment:

   ```bash
   acli ssh myapp.dev
   ```

   For what to *run with* Drush from here, start with [manage configuration](/cloud-platform/code-workflow/configuration/): configuration imports and update hooks are the commands you'll run against an environment most often, and that page covers the order they belong in.

5. ### Automate the loop when it stabilizes

   Once pushing, tagging, and copying data are routine, move them into CI/CD: [Pipelines](/start-here/glossary/#pipelines) builds and tests your application on every push and commits the result as a deployable artifact branch. Start at the [Pipelines quickstart](/cloud-platform/ci-cd/quickstart/); teams already on [Code Studio](/start-here/glossary/#code-studio) stay on it (see [Automate with CI/CD](/cloud-platform/ci-cd/)). For scripted environment operations outside CI (nightly database refreshes, environment creation), use the [Cloud Platform API](/cloud-platform/cli/platform-api/).

</Steps>

## Automate deploy events with Cloud Hooks

The `hooks/` directory from step 1 holds [Cloud Hooks](/start-here/glossary/#cloud-hooks), Cloud Platform's deploy-event mechanism: shell scripts that run on the environment when code or data moves, so "deploy code, then run updates" happens as one unit instead of a checklist. Layout:

```text
hooks/
├── common/                  # every environment
│   └── post-code-update/    # on new commits
│       └── update-db.sh
├── dev/                     # only the dev environment
└── prod/                    # only the prod environment
```

The first directory level picks the environment (`common` for all); the second names the event. The deploy loop runs on two of them: `post-code-update` (new commits landed on the tracked branch) and `post-code-deploy` (the environment switched branch or tag, i.e. `Switch code`). Scripts in an event directory run in alphabetical order; a non-zero exit marks the deploy task failed, which is what you want when updates didn't run.

For the full event list, each event's arguments, and the exact failure semantics, see [Supported hooks](https://github.com/acquia/cloud-hooks#supported-hooks) in the acquia/cloud-hooks repository; for the Cloud Next limits on hooks (memory, timeout, process count), see [Automating with Cloud Hooks](https://docs.acquia.com/acquia-cloud-platform/automating-cloud-hooks) on docs.acquia.com.

The worked example pairs every code change with the update path from the troubleshooting entry below. The code events pass the site name and the target environment as their first two arguments; the [six-argument contract](https://github.com/acquia/cloud-hooks#supported-hooks) has the rest. On a push to a tracked branch, the third and fourth arguments carry the same value. Data-only operations fire their own hooks (`post-db-copy`, `post-files-copy`), so a code hook doesn't need to guard against them:

```bash
#!/bin/sh
# hooks/common/post-code-update/deploy.sh
site="$1"
target_env="$2"

drush @$site.$target_env deploy --yes
```

Make it executable (`chmod +x`) before committing, and place the same script in `post-code-deploy/` so tag switches on production get the identical treatment. `drush deploy` is a single Drush command that runs the post-deploy sequence in the order that works: database updates, then the configuration import, then a cache rebuild. Running those steps by hand in the wrong order is a common way to make a deploy fail; [manage configuration](/cloud-platform/code-workflow/configuration/) covers the sequence and the two contrib modules that keep an unconditional import from doing damage. The hook mechanism that triggers all of it is the Acquia side.

Hook scripts are committed code, not a secret store: a credential or key a hook needs belongs in a [platform environment variable](/cloud-platform/configure/settings/#define-custom-variables-for-your-own-values), never in the script.

One field-tested wrinkle: hooks run as the environment's web user, and the `vendor/bin/drush` shim can lack execute permission on the environment's filesystem. If the alias invocation fails that way, call Drush through PHP directly, resolving the environment's docroot (`/mnt/www/html/<site><env>/docroot`; production has no `<env>` suffix):

```bash
php ${DOCROOT}/../vendor/drush/drush/drush.php --root=${DOCROOT} updb -y
```

## Roll back a release

Prod tracking a tag makes rollback a switch, not a rebuild, but only the code half is automatic. The precondition that makes this runbook safe: **a database backup exists from before the deploy.** Cloud Platform backs up every database daily on its own, but a daily backup is kept for only three days, while an on-demand backup stays until you delete it ([Taking backups](https://docs.acquia.com/acquia-cloud-platform/taking-backups) on docs.acquia.com). Take one as part of any risky deploy. For the platform's own recovery commitments (on Cloud Platform Enterprise and Site Factory, a one-hour recovery point objective for production, snapshot recovery at up to an hour per 50 GB), see [availability and disaster recovery](https://docs.acquia.com/acquia-cloud-platform/cloud-platform-enterprise-availability-and-disaster-recovery) on docs.acquia.com.

```bash
acli api:environments:database-backup-create myapp.prod my_db --task-wait
```

`my_db` is the database's name, not the environment's. `acli api:environments:database-list myapp.prod` prints every database on the environment; the site's own is the entry whose `flags.default` is `true`. The backup runs as a background task, so keep `--task-wait`: with it the command exits zero only once the backup exists, which is what lets a deploy script refuse to continue without one. The Cloud UI has the same action on the environment; see [Taking backups](https://docs.acquia.com/acquia-cloud-platform/taking-backups) on docs.acquia.com.

Every step below is a command, so the runbook runs from a terminal at 2am by whoever is on call.

1. **Identify the previous good tag.** `acli app:vcs:info myapp --deployed` prints the application's deployed branches and tags, one row each, with the environment each is deployed to in the last column: that is what production is running right now. `git tag --sort=-creatordate` in your checkout lists the candidates to go back to.
2. **Decide whether data must roll back too.** The rule: if the bad release ran update hooks or changed configuration on production, the database no longer matches the old code, and code-only rollback can error harder than the bug you're escaping. The Cloud Hook above makes the answer "yes" for any release that shipped updates. No schema or config changes means code-only is safe.
3. **Switch the code back to the previous tag.**

   ```bash
   acli api:environments:code-switch myapp.prod "tags/2026-07-01" --task-wait
   ```

   The `tags/` prefix is required on a tag and wrong on a branch, and this is the step where forgetting it costs the most.
4. **If step 2 said yes, restore the pre-deploy backup.** List the environment's backups, then restore the one taken before the deploy by its `id`:

   ```bash
   acli api:environments:database-backup-list myapp.prod my_db
   acli api:environments:database-backup-restore myapp.prod my_db 1234 --task-wait
   ```

   Each entry in the list carries an `id`, a `type`, and `started_at` and `completed_at` timestamps; the `id` is the third argument to the restore. The backup to restore is the last one whose `completed_at` predates the deploy, not necessarily the newest. Restoring overwrites the database's current contents: content edited since the deploy is lost, so say so to the editorial team before, not after.
5. **Verify**: the site renders, `acli remote:drush myapp.prod -- status` is clean, and a spot-check of recent content matches expectations.

Fix forward on a branch and cut a new tag; never reuse or retag the bad one. A headless frontend rolls back the same way its CI deployed it, by switching to or redeploying the last good artifact: see [multiple environments and a production gate](/source-cms/deploy/external-ci/#multiple-environments-and-a-production-gate).

## When something goes wrong

**Deploy task succeeds but the site errors after a code switch**: code moved, data didn't. A new module or config expects the database state that matches the code. Run the update path against the environment (`acli remote:drush myapp.dev -- updb`), and treat "deploy code, then run updates" as one unit; that pairing is what Cloud Hooks automate.

**`docroot` missing / site serves a directory listing**: the webroot contract is broken; Composer installed Drupal to `web/`. Fix the installer paths in `composer.json` so `index.php` lands in `docroot/`.

**A database copy wiped work on the target environment**: that's the documented overwrite behavior, and why data only flows toward disposable environments. If a backup of the target from before the copy exists (the platform's daily, or one taken on purpose), restore it. The [rollback runbook](#roll-back-a-release) has the two commands (`api:environments:database-backup-list`, then `api:environments:database-backup-restore`). Take an on-demand backup of the target before the next copy.

## Next steps

- [Manage configuration](/cloud-platform/code-workflow/configuration/): the sync directory, importing on deploy, and keeping a deploy from reverting production settings.
- [Set up local development](/cloud-platform/local-dev/quickstart/): the same codebase and data on your machine.
- [Automate with CI/CD](/cloud-platform/ci-cd/quickstart/): the push-to-deploy loop, without you in it.
- [Supported hooks](https://github.com/acquia/cloud-hooks#supported-hooks) in the acquia/cloud-hooks repository: every deploy event, its arguments, and what happens when a hook fails.
- [acli command reference](/cloud-platform/reference/acli/): every command this guide used, with flags.
- [Call the Cloud Platform API](/cloud-platform/cli/platform-api/): the REST API behind the UI actions above.
