Automate B2 Storage in AI/ML Pipelines with the Backblaze B2 GitHub Action
    • Dark
      Light

    Automate B2 Storage in AI/ML Pipelines with the Backblaze B2 GitHub Action

    • Dark
      Light

    Article summary

    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.

    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.

    1. In your GitHub repository, go to Settings > Secrets and variables > Actions.

    2. Click New repository secret and add B2_APPLICATION_KEY_ID with your keyID value.

    3. Add a second secret named B2_APPLICATION_KEY with 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.pt

    When 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

    @v1

    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

    @vX.Y.Z

    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

    @<full-commit-sha>

    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.x

    Common 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.pt

    To 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.gz

    To 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: down

    After 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: delete

    sync 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.pt

    To 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.pt

    Generate 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: 86400

    Reference 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.pt

    Immediately 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.pt

    The 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 verified to false and 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.pt

    In 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: true

    After you have confirmed the file list looks correct, remove dry-run: true and run again to permanently delete.

    Note

    purge removes all file versions under the prefix, including hide markers and version history. An exact-name delete removes only the latest version of one file, but a prefix delete also 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: B2

    SSE-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 32

    Copy 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 bypassGovernance capability by setting bypass-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

    Upload a file or directory to Backblaze B2

    download

    Download a file or prefix from Backblaze B2

    sync

    Mirror a local directory to or from a B2 prefix

    copy

    Server-side copy within or across buckets

    delete

    Delete the latest version of a file or prefix

    purge

    Permanently delete all versions of a file or prefix

    list

    List files under a prefix; emits JSON for downstream steps

    hide

    Soft-delete a file (preserves data until lifecycle runs)

    unhide

    Restore a hidden file

    verify

    Compare remote vs local SHA-1 (no download)

    presign

    Generate a time-limited download URL

    head

    Fetch file metadata only (no download)

    retention

    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

    action

    Required. One of the 13 verbs above.

    application-key-id

    Falls back to $B2_APPLICATION_KEY_ID env var.

    application-key

    Falls back to $B2_APPLICATION_KEY env var.

    bucket

    Required. Destination bucket (for copy, the destination bucket).

    source

    Local path/glob for upload and sync up; B2 path or prefix for everything else.

    destination

    B2 path/prefix for upload, copy, sync up; local path for download, sync down, verify.

    source-bucket

    bucket

    Source bucket for cross-bucket copy.

    dry-run

    false

    Preview without making changes (sync, delete, purge).

    allow-bucket-purge

    false

    Required for purge when source is empty or /.

    presign-ttl

    3600

    Presigned URL TTL in seconds.

    sse

    B2 for SSE-B2, or C:<base64-key> for SSE-C.

    direction

    auto

    Sync direction: auto, up, or down.

    keep-mode

    no-delete

    Sync orphan policy: no-delete, delete, or keep-days.

    keep-days

    Retention window for keep-mode: keep-days.

    expected-sha1

    Literal SHA-1 for verify when you do not want to hash a local destination file.

    retention-mode

    compliance, governance, or none.

    retention-until

    ISO 8601 expiry timestamp (required for compliance and governance).

    legal-hold

    on or off.

    bypass-governance

    false

    Allows governance-mode retention bypass for retention changes and delete/purge operations when the key has the bypassGovernance capability.

    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

    file-id

    upload, copy, hide, retention, head; unhide if a hide marker was removed

    B2 file ID of the affected object or removed hide marker.

    content-sha1

    upload, download, head when available

    SHA-1 hex digest. Omitted when Backblaze B2 does not expose a whole-file SHA-1, including multipart objects.

    bytes-transferred

    upload, download, sync, copy, head

    Total bytes moved. head emits 0.

    file-count

    All verbs

    Aggregate count of files matched or processed.

    files-uploaded

    upload, sync up

    Count of files uploaded.

    files-downloaded

    download, sync down

    Count of files downloaded.

    files-deleted

    delete, purge, sync

    Count of files deleted.

    presigned-url

    presign

    Time-limited download URL. Masked in logs.

    verified

    verify

    true or false.

    remote-sha1

    verify

    The remote object's SHA-1.

    local-sha1

    verify

    Local file SHA-1 when computed from destination.

    summary-json

    All verbs

    Complete JSON array with per-file details when it fits within 256 KiB; emits [] when truncated.

    summary-json-truncated

    All verbs

    true when the full summary-json manifest exceeded the supported size cap.

    retryable

    Failure path

    true when the error is safe to retry automatically.

    retry-after

    Failure path

    Retry delay in seconds when retryable is true.

    Additional Resources

    Help us improve this guide. If you find an error, notice outdated information, or have suggestions for improvement, email techpubs@backblaze.com.


    Was this article helpful?