Skip to main content

Watch SDK

The Watch SDK provides file watching and automatic rebuild functionality. Enable hot-reload development workflows where source file changes automatically trigger rebuilds and process restarts — no manual intervention needed.

Quick start

Watch and rebuild

Add the --watch flag to any build command:
# Watch for changes and rebuild automatically
rbs build //src:app --watch

# Output:
# 🔄 Initial build...
# ✅ Build succeeded
# 👀 Watching for changes... (Ctrl+C to stop)
#    Extensions: [.py .js .ts]
#    Directories: [/workspace/src]

Watch and run

Combine --watch with rbs run to automatically restart your application on changes:
# Watch, rebuild, and restart the process
rbs run //src:server --watch

# Output:
# 🔄 Initial build...
# ✅ Build succeeded
# 🚀 Starting: ./server --port 8080
# ──────────────────────────────────────────────────
# Server listening on :8080
# 👀 Watching for changes... (Ctrl+C to stop)
When you save a file, the watcher detects the change, stops the running process, rebuilds, and restarts:
🔄 Changes detected:
   📝 src/handlers.py
   📝 src/routes.py

⏹️  Stopping process...
🔨 Rebuilding...
✅ Build succeeded in 1.2s
🚀 Starting: ./server --port 8080
──────────────────────────────────────────────────
Server listening on :8080
👀 Watching for changes... (Ctrl+C to stop)

CLI flags

rbs build //target --watch [flags]
rbs run //target --watch [flags]
FlagDescriptionDefault
--watchEnable watch mode.false
--debounce <ms>Milliseconds to wait after last change before rebuilding.300
--clearClear the screen on each rebuild.false
--no-clearDon’t clear the screen on rebuild.
--extensions <ext>Override watched file extensions (comma-separated).Auto-detected

Configuring watch behavior in rules

Rules can specify their own watch behavior using the ctx.watch API. This lets rule authors define sensible defaults for each language or framework.

ctx.watch.config()

Set watch configuration from inside a rule implementation:
def _my_server_impl(ctx):
    # ... build logic ...

    # Configure watch mode for this target
    ctx.watch.config(
        extensions = [".py", ".html", ".css"],
        ignore_patterns = ["__pycache__/", "*.pyc", ".rbs/"],
        extra_watch_dirs = ctx.attr.watch_dirs,
        debounce_ms = 300,
        clear_screen = True,
    )

    return DefaultInfo(executable = output)

Configuration options

OptionTypeDefaultDescription
extensionslist[string][] (all files)File extensions to watch (e.g., .py, .ts).
ignore_patternslist[string][".rbs/", ".git/"]Glob patterns to ignore.
extra_watch_dirslist[string][]Additional directories to watch beyond the target’s sources.
debounce_msint300Wait time in ms after the last change before triggering a rebuild.
clear_screenboolfalseClear terminal screen before each rebuild.
signal_reloadboolfalseSend SIGHUP instead of restarting the process (for servers that support hot reload).

ctx.watch.on_reload()

For servers that support hot reload without a full restart, define a custom reload command:
def _dev_server_impl(ctx):
    # ... build logic ...

    # Support hot reload via custom endpoint
    ctx.watch.on_reload(
        command = "curl -X POST localhost:8080/__reload",
    )

Language examples

Python

# BUILD.rbs
load("@rbs//python/rules.rbs", "py_binary")

py_binary(
    name = "app",
    srcs = ["main.py"],
    deps = [":lib"],
)
rbs run //:app --watch
Watch behavior:
  • Extensions: .py
  • Ignores: __pycache__/, *.pyc
  • Debounce: 300ms

Node.js / TypeScript

# BUILD.rbs
load("@rbs//nodejs/rules.rbs", "nodejs_binary")

nodejs_binary(
    name = "dev_server",
    entry_point = "server.ts",
)
rbs run //:dev_server --watch
Watch behavior:
  • Extensions: .js, .ts, .jsx, .tsx, .css, .html
  • Ignores: node_modules/, dist/
  • Debounce: 200ms

Java / Kotlin (Spring Boot)

# BUILD.rbs
load("@rbs//java/rules.rbs", "java_binary")

java_binary(
    name = "server",
    srcs = glob(["src/**/*.java"]),
    main_class = "com.example.Application",
    deps = [":spring_boot_web"],
)
rbs run //:server --watch
Watch behavior:
  • Extensions: .java, .kt, .properties, .yaml
  • Ignores: target/, build/
  • Debounce: 500ms

Custom watch configuration

def _custom_dev_impl(ctx):
    output = ctx.actions.run(
        command = "make build",
        outputs = [ctx.actions.declare_file("app")],
    )

    ctx.watch.config(
        extensions = [".c", ".h", ".cpp", ".hpp"],
        ignore_patterns = ["build/", "*.o", "*.a"],
        extra_watch_dirs = ["include/", "lib/"],
        debounce_ms = 500,
        clear_screen = True,
    )

    return DefaultInfo(executable = output)

custom_binary = native.define_rule(
    implementation = _custom_dev_impl,
    attrs = {
        "srcs": attr.label_list(),
    },
)

How debouncing works

The watcher uses debouncing to batch rapid file changes into a single rebuild. When multiple files are saved in quick succession (e.g., from an IDE “Save All”), only one rebuild is triggered:
File change: main.py      → Start 300ms timer
File change: utils.py     → Reset timer to 300ms
File change: handlers.py  → Reset timer to 300ms
... timer expires ...
→ Rebuild once with all 3 changes
Recommended debounce settings:
ScenarioRecommended debounce
Fast compile languages (Python, Node.js)200–300ms
Medium compile (Java, Kotlin)300–500ms
Slow compile (C/C++, Rust)500–1000ms
IDE auto-save enabled500ms

Process management

Graceful shutdown

When files change and a process is running, the watcher:
  1. Sends SIGTERM to the process group.
  2. Waits up to 3 seconds for graceful shutdown.
  3. Sends SIGKILL if the process is still running.
  4. Runs the rebuild.
  5. Starts the new process.
Make sure your application handles SIGTERM gracefully — close database connections, flush buffers, and shut down cleanly.

Signal reload (hot reload)

For servers that support hot reload via SIGHUP, enable signal mode to avoid full restarts:
ctx.watch.config(
    signal_reload = True,
)
The watcher sends SIGHUP instead of killing and restarting the process. The server can reload configuration or recompile templates without downtime.

Troubleshooting

Infinite rebuild loop

Symptom: Build triggers, outputs trigger another build, repeat. Solution: Add output directories to ignore patterns:
ctx.watch.config(
    ignore_patterns = ["build/", "dist/", ".rbs/"],
)

Too many open files

Symptom: Error about file descriptor limit. Solution: Reduce the watched directories or increase the file descriptor limit:
ctx.watch.config(
    extra_watch_dirs = ["src/"],  # Watch only source directories, not the entire workspace
)

Process not stopping

Symptom: Old process keeps running after rebuild. Solution: Ensure your application handles SIGTERM and exits cleanly. If your process spawns child processes, make sure they are in the same process group.