---
sidebar_position: 3
title: Bundle build (Heroku-style)
---

# Bundle build

When you `zibby agent deploy`, two things happen:

1. **Source upload** — your agent folder (sources only, no `node_modules`) is sent to S3 via a presigned PUT URL. The CLI does this; the backend never has S3 IAM credentials for your account's bucket.
2. **Bundle build** — a CodeBuild job downloads the sources, runs `npm install --omit=dev`, packages a tarball, uploads it back to S3.

The tarball is what the cloud runtime downloads at trigger time. **No `npm install` happens at runtime** — agents boot in seconds regardless of dependency tree size.

## What's in the bundle

The CLI uploads:

| File | Source | What it does |
|---|---|---|
| `graph.mjs` | `.zibby/workflows/<name>/graph.mjs` | The workflow definition. Required. |
| `nodes/*.mjs` | `.zibby/workflows/<name>/nodes/` | Per-node logic. Optional. |
| `workflow.json` | `.zibby/workflows/<name>/workflow.json` | Manifest (entry class, triggers). Optional. |
| `package.json` + `package-lock.json` | `.zibby/workflows/<name>/` | Dependency resolution. Required when graph.mjs imports `@zibby/*`. |
| `zibby.config.json` | resolved from `.zibby.config.mjs` at project root | Your config (`agent`, `models`, `browser`, …) serialized to JSON. Optional. |

`zibby.config.json` is **resolved locally at deploy time** — the CLI imports your `.zibby.config.mjs`, runs any expressions in it (e.g. `process.env.X`), then `JSON.stringify`s the result. The cloud never executes user JS to load config; it just reads the JSON. Function values are dropped silently — config is data, not code.

This is what makes per-node `models` overrides actually work in cloud:

```js
// .zibby.config.mjs
export default {
  agent: { cursor: { model: 'auto' } },
  models: {
    default: 'auto',
    execute_live: 'claude-opus-4.6',  // overrides JUST the execute_live node
  },
};
```

## Why this matters

Without bundling, every cloud trigger would run `npm install` inside the ECS task. For a typical agent with `@zibby/core` + a few skills, that's 30–90 seconds per cold start. With the bundle:

```
trigger → tarball download (3s) → graph.run() → done
```

3-second cold start instead of 60.

## Spinner output

```
⠋ Building bundle on Zibby Cloud... (provisioning) — 14s
⠙ Building bundle on Zibby Cloud... [1/4] Downloading sources — 22s
⠹ Building bundle on Zibby Cloud... [2/4] Materializing source files — 24s
⠸ Building bundle on Zibby Cloud... [3/4] Installing dependencies — 32s
⠴ Building bundle on Zibby Cloud... [4/4] Packaging bundle — 56s
✔ Bundle ready (78s) — runtime npm install eliminated
```

Pass `--verbose` (or set `ZIBBY_DEPLOY_VERBOSE=1`) to see the raw CodeBuild logs.

## Re-deploys reuse the bundle path

The bundle URL is keyed by agent UUID — re-deploys overwrite the previous bundle. Old executions in flight finish against the *previous* bundle (no rug-pull) because each ECS task downloads the tarball at the moment it starts.

## Security model

The CodeBuild role has **zero S3 IAM permissions**. It receives presigned GET (for sources) and PUT (for bundle) URLs as build-time env vars. URLs are scoped to single keys, expire after 15 minutes.

This means a malicious `postinstall` script in an agent's `package.json` can't escape its sandbox to read other customers' bundles or sources.

## Bundle size

Typical: 100–200 MB compressed. Mostly node_modules. The bundle includes everything `npm install --omit=dev` produces — runtime deps only, no dev deps.

To slim it: audit your agent's `package.json` for unused deps, and prefer the lightest agent SDK that does the job.

## Source-fetch fallback

If the bundle is missing or fails to extract, the cloud runtime falls back to source-fetch + runtime `npm install`. Slower, but agents still run. You'll see this in logs:

```
[setup] Bundle extract failed (...); falling back to source install
[setup] Workflow v3 — 6 source files
[setup] Wrote 6 files
[setup] Installing dependencies...
```

The fallback exists so a deploy that never produced a bundle (e.g. CodeBuild job hadn't completed yet) still works.
