- Print
- DarkLight
Automate B2 Storage in AI/ML Pipelines with the Backblaze B2 GitHub Action
- Print
- DarkLight
AI and ML workflows generate a lot of data that needs to move reliably between machines: training datasets, model checkpoints, fine-tuned weights, evaluation outputs, generated media. Without a storage step baked into your CI pipeline, that data either lives only on the runner (gone when the job ends), gets committed to the repo (too large, wrong tool), or requires manual transfers that break reproducibility.
The Backblaze B2 Cloud Storage GitHub Action (backblaze-labs/b2-action) gives you a single, Backblaze-maintained step that handles all of that storage work directly inside GitHub Actions workflows with no CLI installation, no Docker container, and no wrapper scripts.
Built on the backblaze-labs/b2-sdk-typescript and running on Node 24, this action covers 13 operations: upload, download, sync, copy, delete, purge, list, hide, unhide, verify, presign, head, and retention. The action is Backblaze-maintained and currently incubating in Backblaze Labs. This guide was reviewed against v1.2.0.
Streaming uploads: Files are never fully buffered in RAM, so multi-GB checkpoints and datasets transfer without memory issues on standard runners.
Server-side copy: Promoting a model from a staging bucket to production never routes bytes through the runner.
Structured outputs: Each step emits typed outputs, including file IDs, file names, byte counts, per-verb file counts, and summary-json manifests, that downstream steps can consume.
Secret-safe: App keys, auth tokens, and presigned URLs are automatically masked in logs.
Prerequisites
Make sure the following prerequisites are in place before you begin.
A GitHub repository with GitHub Actions enabled
A Backblaze B2 bucket noting the endpoint URL
A Backblaze B2 application key scoped to the bucket you just created. Do not use your master app key.
Set Up Credentials
GitHub Actions runners do not have access to your Backblaze account by default. You need to store your application key as a GitHub secret so the action can authenticate without exposing credentials in your workflow file.
In your GitHub repository, go to Settings > Secrets and variables > Actions.
Click New repository secret and add
B2_APPLICATION_KEY_IDwith your keyID value.Add a second secret named
B2_APPLICATION_KEYwith your applicationKey value.
Reference them in any workflow as ${{ secrets.B2_APPLICATION_KEY_ID }} and ${{ secrets.B2_APPLICATION_KEY }}. Never paste key values directly into a workflow file.
Add the Action to a Workflow
GitHub Actions workflows are YAML files that live in .github/workflows/ in your repository. Each workflow contains one or more jobs, and each job contains steps. The b2-action is a step you add anywhere in a job. The only required inputs are action (which operation to run) and bucket (which Backblaze B2 bucket to use). Everything else depends on the operation.
Here is a minimal example that uploads a file:
- uses: backblaze-labs/b2-action@v1
with:
action: upload
application-key-id: ${{ secrets.B2_APPLICATION_KEY_ID }}
application-key: ${{ secrets.B2_APPLICATION_KEY }}
bucket: my-ml-bucket
source: ./checkpoints/epoch-10.pt
destination: runs/${{ github.run_id }}/epoch-10.ptWhen the workflow runs, you will see a summary table in the Actions UI with the outcome, file ID, and bytes transferred.
Pin the Action for Production Use
The action follows semantic versioning. The version reference you use in uses: controls how stable and auditable your workflow is. It is worth choosing deliberately before deploying to production.
Choose one of the following reference styles based on how your workflow will be used.
Reference style | Behavior | When to use it |
|---|---|---|
| Tracks the latest 1.x release. Picks up patches automatically, but the tag is mutable, meaning the code that runs can change without any change to your workflow file. | Experimentation and development |
| A pinned, SSH-signed release tag that shows as Verified on GitHub. Stable and easy to audit, but a Git tag is still a movable ref. | Staging and controlled production |
| Fully immutable. Guarantees the exact code that runs cannot change, even if a tag is moved. If you enable Dependabot for GitHub Actions, it keeps the SHA current automatically. | Production (recommended) |
To pin to a full commit SHA:
# Pin to a specific commit SHA for full immutability.
# Dependabot will bump this automatically if enabled.
- uses: backblaze-labs/b2-action@<full-commit-sha> # v1.x.xCommon Tasks
The following tasks cover the most common AI/ML storage patterns. Add whichever steps apply to your workflow.
Save a Model Checkpoint After Training
Training jobs on ephemeral CI runners lose all local files when the job ends. Uploading checkpoints to Backblaze B2 during or after training means you can resume from a known state, compare runs, and roll back to an earlier version, all without keeping a runner alive.
At the end of your training job, add the following step to your workflow YAML. Replace my-ml-bucket with your bucket name:
- uses: backblaze-labs/b2-action@v1
with:
action: upload
application-key-id: ${{ secrets.B2_APPLICATION_KEY_ID }}
application-key: ${{ secrets.B2_APPLICATION_KEY }}
bucket: my-ml-bucket
source: ./checkpoints/epoch-10.pt
destination: runs/${{ github.run_id }}/checkpoints/epoch-10.ptTo upload an entire checkpoint directory instead of a single file, point source at the folder:
- uses: backblaze-labs/b2-action@v1
with:
action: upload
application-key-id: ${{ secrets.B2_APPLICATION_KEY_ID }}
application-key: ${{ secrets.B2_APPLICATION_KEY }}
bucket: my-ml-bucket
source: ./checkpoints
destination: runs/${{ github.run_id }}/checkpoints/Using ${{ github.run_id }} in the destination path keeps each run's artifacts in their own prefix, making them easy to find and compare later.
Pull a Dataset or Model Weights Before a Job
Training and fine-tuning jobs need their input data available on the runner before they start. Storing datasets and base model weights in Backblaze B2 gives every job a consistent, versioned starting point, regardless of which runner picks up the job or when it runs.
At the start of your job, before any training or inference steps, add a download step. Replace the source path with the Backblaze B2 key for your dataset or weights file:
- uses: backblaze-labs/b2-action@v1
with:
action: download
application-key-id: ${{ secrets.B2_APPLICATION_KEY_ID }}
application-key: ${{ secrets.B2_APPLICATION_KEY }}
bucket: my-ml-bucket
source: datasets/imagenet-subset.tar.gz
destination: ./data/imagenet-subset.tar.gzTo download an entire directory of files (for example, all shards of a dataset or all files for a model) add a trailing slash to the source path:
- uses: backblaze-labs/b2-action@v1
with:
action: download
application-key-id: ${{ secrets.B2_APPLICATION_KEY_ID }}
application-key: ${{ secrets.B2_APPLICATION_KEY }}
bucket: my-ml-bucket
source: models/llama-3-finetuned/
destination: ./models/Cache a Model or Dataset Across Runs
Downloading large model weights or datasets from scratch on every run wastes time and egress. Caching in Backblaze B2 lets you persist the local runner cache between runs, so subsequent jobs only pull what has changed. This is especially useful for Hugging Face model caches, which can be several GB and contain many small files.
The pattern uses two steps: restore the cache at the start of the job, then save it at the end.
At the start of your job, add a sync step to pull the cache from Backblaze B2 to the runner:
- uses: backblaze-labs/b2-action@v1
with:
action: sync
application-key-id: ${{ secrets.B2_APPLICATION_KEY_ID }}
application-key: ${{ secrets.B2_APPLICATION_KEY }}
bucket: my-ml-bucket
source: caches/${{ runner.os }}/huggingface/
destination: ~/.cache/huggingface
direction: downAfter your training or inference steps, add a sync step to push any new or changed files back to Backblaze B2:
- uses: backblaze-labs/b2-action@v1
with:
action: sync
application-key-id: ${{ secrets.B2_APPLICATION_KEY_ID }}
application-key: ${{ secrets.B2_APPLICATION_KEY }}
bucket: my-ml-bucket
source: ~/.cache/huggingface
destination: caches/${{ runner.os }}/huggingface/
direction: up keep-mode: deletesync only transfers files that have changed since the last run, so subsequent runs get progressively faster as the cache stabilizes. keep-mode: delete removes files from Backblaze B2 that no longer exist locally, keeping the remote cache clean.
Promote a Model from Staging to Production
After evaluation, you need to move a validated model from a staging bucket to production. Downloading and re-uploading routes gigabytes of model data through the runner unnecessarily, slowing the pipeline and consuming egress. Server-side copy moves the file entirely within Backblaze B2. The runner just issues the instruction.
Add a copy step to your deployment or promotion workflow. Set bucket to your production bucket, source-bucket to your staging bucket, and update the source and destination paths for your model. The application key must have read access to the source bucket and write access to the destination bucket:
- uses: backblaze-labs/b2-action@v1
with:
action: copy
application-key-id: ${{ secrets.B2_APPLICATION_KEY_ID }}
application-key: ${{ secrets.B2_APPLICATION_KEY }}
bucket: models-prod
source-bucket: models-staging
source: llama-3-finetuned-v2.pt
destination: llama-3-finetuned-latest.ptTo copy within the same bucket (for example, to tag the current best checkpoint as latest) omit source-bucket:
- uses: backblaze-labs/b2-action@v1
with:
action: copy
application-key-id: ${{ secrets.B2_APPLICATION_KEY_ID }}
application-key: ${{ secrets.B2_APPLICATION_KEY }}
bucket: my-ml-bucket
source: runs/${{ github.run_id }}/checkpoints/epoch-10.pt
destination: models/best-checkpoint.ptGenerate a Presigned URL to Share an Artifact
Evaluation reports, generated media, and run summaries stored in a private Backblaze B2 bucket are not directly accessible to people or systems outside your pipeline. A presigned URL creates a time-limited link to a specific file that anyone can access without needing Backblaze B2 credentials. This is useful for Slack notifications, emails, or downstream systems that need to fetch the file.
Add a presign step and give it an id so you can reference the URL in later steps. Set presign-ttl to how long (in seconds) the link should remain valid:
- id: share
uses: backblaze-labs/b2-action@v1
with:
action: presign
application-key-id: ${{ secrets.B2_APPLICATION_KEY_ID }}
application-key: ${{ secrets.B2_APPLICATION_KEY }}
bucket: my-ml-bucket
source: runs/${{ github.run_id }}/eval-report.html
presign-ttl: 86400Reference the URL in a subsequent step using ${{ steps.share.outputs.presigned-url }}. Here is an example that posts it to a Slack webhook:
- name: Post report link to Slack
env:
REPORT_URL: ${{ steps.share.outputs.presigned-url }}
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
run: |
curl -X POST "$SLACK_WEBHOOK" \
-H "Content-Type: application/json" \
-d "{\"text\": \"Eval report: $REPORT_URL\"}"
steps.share.outputs.presigned-url }}\"}"The URL is automatically masked in logs. It will not appear in plaintext even if you echo it directly.
Verify a Checkpoint Was Not Corrupted in Transit
Large file transfers can silently produce a corrupted file that appears complete. For model checkpoints, a corrupted file might load without error but produce wrong outputs. Verifying the SHA-1 digest after upload confirms the remote file is bit-for-bit identical to the local source without downloading the file again.
Upload the checkpoint as normal, giving the step an id:
- id: upload
uses: backblaze-labs/b2-action@v1
with:
action: upload
application-key-id: ${{ secrets.B2_APPLICATION_KEY_ID }}
application-key: ${{ secrets.B2_APPLICATION_KEY }}
bucket: my-ml-bucket
source: ./checkpoints/epoch-10.pt
destination: runs/${{ github.run_id }}/epoch-10.ptImmediately after, add a verify step. Set source to the B2 path and destination to the local file you just uploaded from:
- uses: backblaze-labs/b2-action@v1
with:
action: verify
application-key-id: ${{ secrets.B2_APPLICATION_KEY_ID }}
application-key: ${{ secrets.B2_APPLICATION_KEY }}
bucket: my-ml-bucket
source: runs/${{ github.run_id }}/epoch-10.pt
destination: ./checkpoints/epoch-10.ptThe verify step sends only a HEAD request to fetch the remote SHA-1, with no body download. The step fails and the workflow stops if the hashes do not match.
Note
Verify requires a whole-file SHA-1 from Backblaze B2. Multipart-uploaded objects may not have one, and the action sets
verifiedtofalseand fails closed when that happens.
Use Step Outputs Downstream
Action steps produce structured outputs such as B2 file IDs, file names, SHA-1 digests, byte counts, and per-verb counts. The exact outputs vary by verb. These are useful for logging to a model registry, triggering downstream systems, or building audit trails without having to re-query Backblaze B2 for the information.
Give the action step an id:
- id: upload
uses: backblaze-labs/b2-action@v1
with:
action: upload
application-key-id: ${{ secrets.B2_APPLICATION_KEY_ID }}
application-key: ${{ secrets.B2_APPLICATION_KEY }}
bucket: my-ml-bucket
source: ./checkpoints/epoch-10.pt
destination: runs/${{ github.run_id }}/epoch-10.ptIn any later step, reference the outputs using ${{ steps.\<id\>.outputs.\<output-name\> }}, and pass them through env before using them in shell commands:
- name: Record upload metadata
env:`
FILE_ID: ${{ steps.upload.outputs.file-id }}
FILE_SHA1: ${{ steps.upload.outputs.content-sha1 }}
FILE_BYTES: ${{ steps.upload.outputs.bytes-transferred }}
run: |
echo "File ID : $FILE_ID"
echo "SHA-1 : $FILE_SHA1"
echo "Bytes : $FILE_BYTES"See the Backblaze B2 GitHub Action for the full list of available outputs and which verbs produce them.
Clean Up Old Run Artifacts
AI/ML pipelines can accumulate a large volume of run artifacts quickly. Checkpoints from every epoch of every run add up fast. Purging old run data keeps storage costs down and buckets organized, but you want to be careful about what you delete. The dry-run option lets you see exactly what would be removed before committing.
Add a purge step with dry-run: true and run your workflow. Review the step summary in the Actions UI to confirm the list of files that would be deleted:
- uses: backblaze-labs/b2-action@v1
with:
action: purge
application-key-id: ${{ secrets.B2_APPLICATION_KEY_ID }}
application-key: ${{ secrets.B2_APPLICATION_KEY }}
bucket: my-ml-bucket
source: runs/old-run-id/
dry-run: trueAfter you have confirmed the file list looks correct, remove dry-run: true and run again to permanently delete.
Note
purgeremoves all file versions under the prefix, including hide markers and version history. An exact-namedeleteremoves only the latest version of one file, but a prefixdeletealso removes every version under that prefix.
After you confirm that the file list looks correct, remove dry-run: true and run again to permanently delete.
Encryption
Proprietary model weights and sensitive training datasets may require encryption at rest for compliance, IP protection, or organizational policy. The action supports two modes of server-side encryption with the sse input. Use sse: B2 on uploads for Backblaze-managed encryption, or sse: C:<base64-key> on uploads and downloads for customer-managed encryption (SSE-C).
SSE-B2 (Simplest — No Key Management)
Backblaze generates and manages the encryption key. There is nothing to set up beyond adding sse: B2 to your step. If you lose access to your Backblaze B2 account, Backblaze can still recover the data.
Add sse: B2 to any upload step:
- uses: backblaze-labs/b2-action@v1
with:
action: upload
application-key-id: ${{ secrets.B2_APPLICATION_KEY_ID }}
application-key: ${{ secrets.B2_APPLICATION_KEY }}
bucket: my-ml-bucket
source: ./models/proprietary-weights.pt
destination: models/proprietary-weights.pt
sse: B2SSE-C (You Hold the Key)
You provide a 256-bit encryption key. Backblaze never stores it. This gives you full control over who can decrypt the data, but it means you are solely responsible for keeping the key safe.
Run the following command in your terminal to generate a 256-bit key:
openssl rand -base64 32Copy the output (a 44-character string like JXqRk7TZUyDhPmlAv9pn0WzgQGkBNyfwHJtoMSCRXNc=) and add it as a GitHub repository secret named B2_SSE_C_KEY_B64.
Reference it in your upload and download steps:
- uses: backblaze-labs/b2-action@v1
with:
action: upload
application-key-id: ${{ secrets.B2_APPLICATION_KEY_ID }}
application-key: ${{ secrets.B2_APPLICATION_KEY }}
bucket: my-ml-bucket
source: ./models/proprietary-weights.pt
destination: models/proprietary-weights.pt
sse: C:${{ secrets.B2_SSE_C_KEY_B64 }}Important
Backblaze never stores your SSE-C key. You must supply the same key at download time. If you lose the key, the data is permanently unrecoverable; there is no recovery path. The action masks the key in logs automatically, but keep it out of bug reports and issue comments.
Object Lock
Some AI workflows have compliance or legal requirements around data retention. For example, you may need to keep training data provenance records immutable for a set period, or preserve model artifacts that informed a regulated decision. Object Lock lets you write a file to Backblaze B2 and then prevent anyone, including bucket owners, from modifying or deleting it until a specified date.
Object Lock requires a bucket with Object Lock enabled. You can enable this when creating a bucket in the Backblaze console.
Upload the file you want to lock to Backblaze B2 first if it is not already there, then add a retention step. Set retention-mode to compliance or governance, and set retention-until to an ISO 8601 timestamp for when the lock should expire:
- uses: backblaze-labs/b2-action@v1
with:
action: retention
application-key-id: ${{ secrets.B2_APPLICATION_KEY_ID }}
application-key: ${{ secrets.B2_APPLICATION_KEY }}
bucket: my-locked-bucket
source: audits/training-run-2026-q2.tar.gz
retention-mode: compliance
retention-until: '2031-04-01T00:00:00Z'
legal-hold: 'on'The two retention modes behave differently:
compliance: The lock cannot be shortened or removed by anyone, including Backblaze, until the date passes. Use this for regulatory requirements.
governance: The lock can be overridden by users with the
bypassGovernancecapability by settingbypass-governance: true. Use this for internal policy enforcement where you may need an override path.
Reference
This section lists all 13 verbs the action supports, along with the full inputs and outputs reference. Use it to look up available operations and configure steps beyond the common tasks covered above.
All 13 Verbs
Each verb maps to a single Backblaze B2 operation and is set via the action input. The following table lists all 13 and what they do.
Verb | What it does |
|---|---|
| Upload a file or directory to Backblaze B2 |
| Download a file or prefix from Backblaze B2 |
| Mirror a local directory to or from a B2 prefix |
| Server-side copy within or across buckets |
| Delete the latest version of a file or prefix |
| Permanently delete all versions of a file or prefix |
| List files under a prefix; emits JSON for downstream steps |
| Soft-delete a file (preserves data until lifecycle runs) |
| Restore a hidden file |
| Compare remote vs local SHA-1 (no download) |
| Generate a time-limited download URL |
| Fetch file metadata only (no download) |
| Apply Object Lock retention and/or legal hold |
Key Inputs
Key Inputs
The following table covers the inputs used most commonly across the tasks in this guide. For the full list of every available input and its accepted values, see the Backblaze B2 GitHub Action guide.
Input | Default | Notes |
|---|---|---|
| — | Required. One of the 13 verbs above. |
| — | Falls back to |
| — | Falls back to |
| — | Required. Destination bucket (for |
| — | Local path/glob for |
| — | B2 path/prefix for |
|
| Source bucket for cross-bucket |
|
| Preview without making changes ( |
|
| Required for |
|
| Presigned URL TTL in seconds. |
| — |
|
|
| Sync direction: |
|
| Sync orphan policy: |
| — | Retention window for |
| — | Literal SHA-1 for |
| — |
|
| — | ISO 8601 expiry timestamp (required for |
| — |
|
|
| Allows governance-mode retention bypass for retention changes and |
Key Outputs
Every action step emits structured outputs that downstream steps can reference using ${{ steps.<id>.outputs.<output-name> }}. The following table covers the most useful outputs. For the complete list, see the Backblaze B2 GitHub Action guide.
Output | Available on | Description |
|---|---|---|
|
| B2 file ID of the affected object or removed hide marker. |
|
| SHA-1 hex digest. Omitted when Backblaze B2 does not expose a whole-file SHA-1, including multipart objects. |
|
| Total bytes moved. |
| All verbs | Aggregate count of files matched or processed. |
|
| Count of files uploaded. |
|
| Count of files downloaded. |
|
| Count of files deleted. |
|
| Time-limited download URL. Masked in logs. |
|
|
|
|
| The remote object's SHA-1. |
|
| Local file SHA-1 when computed from |
| All verbs | Complete JSON array with per-file details when it fits within 256 KiB; emits |
| All verbs |
|
| Failure path |
|
| Failure path | Retry delay in seconds when |
Additional Resources
Source code, issues, and pull requests
Install the action from the Marketplace
Example workflows
One runnable example per verb, each is also a live integration testChangelog
Release history and version notesBackblaze Labs
Backblaze B2 integrations and developer examples
Help us improve this guide. If you find an error, notice outdated information, or have suggestions for improvement, email techpubs@backblaze.com.