BackgroundWorkerApp, implement a handful of methods, and call app.main(). The runtime handles the schedule, retries, and the gRPC plumbing.
The richer and more concrete your submissions are, the better proactive behavior you’ll get. “FED-RATE-SEP moved up 12c (was 41c, now 53c)” routes much better than “market alert”.
Background patterns in the examples
The bundled examples use background apps in a few distinct ways:
The common pattern is: keep API/auth clients in shared modules, keep polling/diffing logic in a plain worker, and keep runtime submission logic in the
BackgroundWorkerApp subclass.
The worker + app split
Reference apps consistently keep the work (API calls, parsing, diffing) in a plainWorker class, and the SDK wiring in a small BackgroundWorkerApp subclass. This makes the worker easy to unit test without touching the runtime.
Minimal example
The worker (bg_worker.py)
The worker knows nothing about the SDK. It returns a CycleResult dataclass describing what happened.
The app (my_app_background.py)
The BackgroundWorkerApp base class
Subclass BackgroundWorkerApp[WorkerT, ResultT] with the two types of your worker and your cycle result. Call super().__init__(name, logger_name=...).
You implement four methods:
And you get these helpers for free:
app.main() and --verify
app.main() is the entry point:
-
Normal mode — starts the background loop and blocks. The runtime calls
run_cycle()on each schedule tick. -
--verifymode — when--verifyis insys.argv,main()callsverify_worker(), prints the result, and exits with0(ok) or1(failed).truffile deployuses this to check credentials immediately after yourtextstep collects them:
Handling cycle results
handle_cycle_result(ctx, result) is where you decide what to do with each cycle’s output. The pattern is roughly:
- If it’s an auth error, report it (deduped) and return.
- Otherwise, reset the auth failure counter.
- If it’s a runtime error, log it and return. (The runtime will still retry next cycle.)
- If nothing changed, return without submitting.
- Otherwise, call
submit_text(...)with a well-formed context string.
Auth failure dedup
Background apps poll often. If a token goes bad, you don’t want a noisy failure report every 30 minutes — you want one clear report once the situation is clearly stuck.self.report_auth_failure(ctx, description) handles this for you:
- Logs a warning on the first few failures
- After
AUTH_FAILURE_THRESHOLD(default: 3) consecutive failures, reports to the runtime withneeds_intervention=Trueso the user is prompted - Subsequent failures stay suppressed until a successful cycle calls
self.reset_auth_failures()
AUTH_FAILURE_THRESHOLD on your subclass if 3 is too few or too many.
Calling your own foreground tools from the background
If your app already has a foreground with tools likegmail_search, github_list_issues, or kalshi_get_positions, your background doesn’t need to reimplement that logic — it can call its own foreground tools directly and reuse all the auth, parsing, and error handling that lives there.
The runtime gives your background a lease on the foreground container via ctx.connect_foreground(). It returns a ForegroundConnection that speaks MCP to your foreground.
Basic shape
ctx.connect_foreground()is async, so your cycle has to be too. The standard pattern is a sync entry point (def my_background(ctx)) that callsasyncio.run(_run_cycle(ctx))into an async worker.fg.call_tool(name, **kwargs)takes the tool name and any keyword arguments. The kwargs become the tool’s input — exactly the same as if the agent had called it.- Always call
await fg.close()in afinallyblock so the lease is released when the cycle ends.
Because this pattern needs an async function with direct access to
ctx, it’s easiest to write with the lower-level run_background(fn) entry point instead of BackgroundWorkerApp.Real example: reuse foreground auth
The most common reason to reach into the foreground is to reuse its auth check. Instead of rewriting token loading and validation inbg_worker.py, just call your foreground’s check_auth tool:
What call_tool returns
call_tool(name, **kwargs) returns an MCP CallToolResult object. The result content is a list of content blocks — for text tools, that’s usually one or more TextContent blocks. A small helper to extract the text:
ok(...) / err(...), parse the extracted text with json.loads.
Listing available tools
If you need to discover what tools the foreground exposes (useful during development or for dynamic dispatch):Always close the connection
Wrap your foreground calls in atry / finally and call await fg.close() when you’re done — even on exceptions. Skipping this leaves the foreground container held open longer than it needs to be.
Using this with BackgroundWorkerApp
The run_background(fn) entry point is the cleanest fit for foreground calls because it’s async-native. If you’re already using BackgroundWorkerApp for a simpler cycle and want to mix in a foreground call, bridge from sync to async inside handle_cycle_result:
run_background(fn) pattern end-to-end.
Writing good context submissions
The quality of your background context directly affects how proactive behavior feels. Rich, concrete items win every time. Weak:Market alertStrong:
Price alert: FED-RATE-SEP moved up 12c (was 41c, now 53c) Related: https://kalshi.com/markets/FED-RATE-SEPWeak:
New PRs in your reposStrong:
@octocat opened PR #342 in truffle-ai/pyfw-codex: “Fix mDNS timeout on retry” 2 files changed, +18/-4. Requested reviews: @alice, @bob https://github.com/truffle-ai/pyfw-codex/pull/342A few guidelines:
- Include the entities. Names, tickers, IDs, usernames, repo paths, order numbers.
- Include numbers with context.
$12kalone is useless;$12k (was $8.4k yesterday)is actionable. - Include links. Pass them in
uris=(...)so the agent can open them if it decides to act. - Pick priority honestly. Default priority is fine for most things. Bump it for genuine urgency (alerts, settlements, revoked tokens).
- Don’t spam. Use a fingerprint or diff to skip cycles where nothing meaningfully changed — return a
CycleResultwithchanged=Falseand don’t submit.
Background context can trigger action in any app, not just yours. If an Instagram message about an Amazon order lands in the proactive agent’s context, Truffle might use your Amazon app to add the item to cart. Write submissions with that in mind.
Schedule configuration
Thedefault_schedule block in truffile.yaml under metadata.background controls when your cycle runs. Three policy types are supported: interval, always, and times.
interval — run every N minutes/hours
The most common mode. Your cycle runs, yields, sleeps for the configured duration, then runs again.
duration— interval between runs. Acceptsms,s,m,h,dsuffixes (e.g.5m,30m,2h,1d).schedule.daily_window— optional time window that gates execution."09:00-18:00"only runs during business hours;"00:00-23:59"(the effective default) runs any time.schedule.allowed_days— optional list like[mon, tue, wed, thu, fri]to restrict to specific weekdays.schedule.forbidden_days— optional inverse: list of days to skip (e.g.[sat, sun]). Mutually exclusive withallowed_days.
always — keep running as long as there’s work
For apps that need to react to live data the moment it arrives. The runtime keeps your process alive continuously instead of running-then-sleeping. Use it when a fixed interval would either miss updates (too slow) or burn cycles (too fast).
type: always.
Good fits:
- Watching a websocket or server-sent event stream (market data, chat messages, push notifications)
- Apps that need to submit context to the proactive agent the moment something happens — e.g. a Kalshi watcher that fires a high-priority submission the instant a position moves, not 30 minutes later
- Any integration where the upstream service pushes data instead of you polling for it
times — run at specific times of day
For apps that should run at fixed clock times (e.g. a daily 9am digest):
run_times— list ofHH:MM(orHH:MM:SS) times of day, each triggers one cycle.allowed_days/forbidden_days— optional day-of-week gating, same semantics asinterval.
Cleanup on shutdown
Close your worker’s network clients on exit:Deploy and verify
- Your
run_cycle()will be invoked on the configured interval handle_cycle_result()decides what to submitsubmit_text(...)calls land in the proactive agent’s context- Auth failures past the threshold surface as “needs intervention” in the app UI
When to build a background app
Use a background app when you want:- Periodic polling of an external system (new messages, new PRs, new fills, new feed items)
- Monitoring with thresholds (price alerts, rate changes, error budgets)
- Digest generation (daily/weekly/hourly summaries)
- Context emission that feeds proactive actions in other apps
foreground section to the same truffile.yaml — it becomes a hybrid app. Use the same shared client/auth module from both entry points, and keep background-specific diffing or polling logic in a plain worker class so it stays easy to test.