Skip to main content
Background apps run on a schedule and feed context into Truffle’s proactive agent. Foreground apps are callable tools; background apps are context producers. You subclass 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 plain Worker 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)

That’s the whole pattern. Everything below explains each method.

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:
It handles two modes automatically:
  • Normal mode — starts the background loop and blocks. The runtime calls run_cycle() on each schedule tick.
  • --verify mode — when --verify is in sys.argv, main() calls verify_worker(), prints the result, and exits with 0 (ok) or 1 (failed). truffile deploy uses this to check credentials immediately after your text step 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:
  1. If it’s an auth error, report it (deduped) and return.
  2. Otherwise, reset the auth failure counter.
  3. If it’s a runtime error, log it and return. (The runtime will still retry next cycle.)
  4. If nothing changed, return without submitting.
  5. 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 with needs_intervention=True so the user is prompted
  • Subsequent failures stay suppressed until a successful cycle calls self.reset_auth_failures()
You can override 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 like gmail_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

Key points:
  • 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 calls asyncio.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 a finally block 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 in bg_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:
If your foreground tools return JSON with 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 a try / 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:
The tradeoff is one extra event loop per cycle. For apps that mostly live in foreground calls, prefer the 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 alert
Strong:
Price alert: FED-RATE-SEP moved up 12c (was 41c, now 53c) Related: https://kalshi.com/markets/FED-RATE-SEP
Weak:
New PRs in your repos
Strong:
@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/342
A few guidelines:
  • Include the entities. Names, tickers, IDs, usernames, repo paths, order numbers.
  • Include numbers with context. $12k alone 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 CycleResult with changed=False and 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

The default_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. Accepts ms, s, m, h, d suffixes (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 with allowed_days.
During development, set duration: 1m so you can see results quickly. Bump it back up before shipping.

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).
No duration, no schedule block — just 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
Under always, your cycle function is expected to block (on a websocket read, a queue, whatever) rather than return quickly. If you return immediately, the runtime will just call you again and you’ll burn CPU doing nothing. Write your loop to stay inside run_cycle for as long as the connection is alive, and return only when you want the runtime to restart you.

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 of HH:MM (or HH:MM:SS) times of day, each triggers one cycle.
  • allowed_days / forbidden_days — optional day-of-week gating, same semantics as interval.

Cleanup on shutdown

Close your worker’s network clients on exit:
This isn’t optional. Leaving HTTP connections and other resources open at process exit is the most common source of flaky redeploys.

Deploy and verify

Once deployed:
  • Your run_cycle() will be invoked on the configured interval
  • handle_cycle_result() decides what to submit
  • submit_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
If you also need callable tools, add a 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.