---
title: Export and save D1 database · Cloudflare Workflows docs
description: Send invoice when shopping cart is checked out and paid for
lastUpdated: 2025-03-10T13:45:35.000Z
source_url:
  html: https://zt-dark-mode-changelog.preview.developers.cloudflare.com/workflows/examples/backup-d1/index.md
  md: https://zt-dark-mode-changelog.preview.developers.cloudflare.com/workflows/examples/backup-d1/
---

In this example, we implement a Workflow periodically triggered by a [Cron Trigger](https://developers.cloudflare.com/workers/configuration/cron-triggers). That Workflow initiates a backup for a D1 database using the REST API, and then stores the SQL dump in an [R2](https://developers.cloudflare.com/r2) bucket.

When the Workflow is triggered, it fetches the REST API to initiate an export job for a specific database. Then it fetches the same endpoint to check if the backup job is ready and the SQL dump is available to download.

As shown in this example, Workflows handles both the responses and failures, thereby removing the burden from the developer. Workflows retries the following steps:

* API calls until it gets a successful response
* Fetching the backup from the URL provided
* Saving the file to [R2](https://developers.cloudflare.com/r2)

The Workflow can run until the backup file is ready, handling all of the possible conditions until it is completed.

This example provides simplified steps for backing up a [D1](https://developers.cloudflare.com/d1) database to help you understand the possibilities of Workflows. In every step, it uses the [default](https://developers.cloudflare.com/workflows/build/sleeping-and-retrying) sleeping and retrying configuration. In a real-world scenario, more steps and additional logic would likely be needed.

```ts
import {
  WorkflowEntrypoint,
  WorkflowStep,
  WorkflowEvent,
} from "cloudflare:workers";


// We are using R2 to store the D1 backup
type Env = {
  BACKUP_WORKFLOW: Workflow;
  D1_REST_API_TOKEN: string;
  BACKUP_BUCKET: R2Bucket;
};


// Workflow parameters: we expect accountId and databaseId
type Params = {
  accountId: string;
  databaseId: string;
};


// Workflow logic
export class backupWorkflow extends WorkflowEntrypoint<Env, Params> {
  async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
    const { accountId, databaseId } = event.payload;


    const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/d1/database/${databaseId}/export`;
    const method = "POST";
    const headers = new Headers();
    headers.append("Content-Type", "application/json");
    headers.append("Authorization", `Bearer ${this.env.D1_REST_API_TOKEN}`);


    const bookmark = await step.do(
      `Starting backup for ${databaseId}`,
      async () => {
        const payload = { output_format: "polling" };


        const res = await fetch(url, {
          method,
          headers,
          body: JSON.stringify(payload),
        });
        const { result } = (await res.json()) as any;


        // If we don't get `at_bookmark` we throw to retry the step
        if (!result?.at_bookmark) throw new Error("Missing `at_bookmark`");


        return result.at_bookmark;
      },
    );


    await step.do("Check backup status and store it on R2", async () => {
      const payload = { current_bookmark: bookmark };


      const res = await fetch(url, {
        method,
        headers,
        body: JSON.stringify(payload),
      });
      const { result } = (await res.json()) as any;


      // The endpoint sends `signed_url` when the backup is ready to download.
      // If we don't get `signed_url` we throw to retry the step.
      if (!result?.signed_url) throw new Error("Missing `signed_url`");


      const dumpResponse = await fetch(result.signed_url);
      if (!dumpResponse.ok) throw new Error("Failed to fetch dump file");


      // Finally, stream the file directly to R2
      await this.env.BACKUP_BUCKET.put(result.filename, dumpResponse.body);
    });
  }
}


export default {
  async fetch(req: Request, env: Env): Promise<Response> {
    return new Response("Not found", { status: 404 });
  },
  async scheduled(
    controller: ScheduledController,
    env: Env,
    ctx: ExecutionContext,
  ) {
    const params: Params = {
      accountId: "{accountId}",
      databaseId: "{databaseId}",
    };
    const instance = await env.BACKUP_WORKFLOW.create({ params });
    console.log(`Started workflow: ${instance.id}`);
  },
};
```

Here is a minimal package.json:

```json
{
  "devDependencies": {
    "@cloudflare/workers-types": "^4.20241224.0",
    "wrangler": "^3.99.0"
  }
}
```

Here is a [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/):

* wrangler.jsonc

  ```jsonc
  {
    "name": "backup-d1",
    "main": "src/index.ts",
    "compatibility_date": "2024-12-27",
    "compatibility_flags": [
      "nodejs_compat"
    ],
    "workflows": [
      {
        "name": "backup-workflow",
        "binding": "BACKUP_WORKFLOW",
        "class_name": "backupWorkflow"
      }
    ],
    "r2_buckets": [
      {
        "binding": "BACKUP_BUCKET",
        "bucket_name": "d1-backups"
      }
    ],
    "triggers": {
      "crons": [
        "0 0 * * *"
      ]
    }
  }
  ```

* wrangler.toml

  ```toml
  name = "backup-d1"
  main = "src/index.ts"
  compatibility_date = "2024-12-27"
  compatibility_flags = [ "nodejs_compat" ]


  [[workflows]]
  name = "backup-workflow"
  binding = "BACKUP_WORKFLOW"
  class_name = "backupWorkflow"


  [[r2_buckets]]
  binding = "BACKUP_BUCKET"
  bucket_name = "d1-backups"


  [triggers]
  crons = [ "0 0 * * *" ]
  ```
