Long-running operations
Long-running operations — background tasks and automatic chat delivery for AI calls and slow work that runs beyond the chat timeout, then lands back in chat.
Some operations legitimately take longer than the 30-second default ctx.http timeout — AI generation, multi-stage retrieval, large file transfers, third-party batch APIs. Imperal Cloud gives you a tiered set of SDK surfaces, shipped together in v4.2.12.
This page is the mental model. For copy-paste patterns, see Long-running AI calls.
The surfaces
| Surface | When to reach for it | Cap |
|---|---|---|
ctx.http(..., timeout=N) | A single HTTP call legitimately needs 30–180 seconds. | 180s |
ctx.background_task(coro, long_running=False) (v4.2.12+) | The whole handler body — or its slow tail — should run detached so chat unblocks immediately, with the result delivered to chat when it finishes. | 180s default; 1800s with long_running=True |
ctx.deliver_chat_message(text) | You want to inject a bot message into the user's chat outside a normal handler reply (OAuth callback ack, mid-stage announcement, one-off proactive note). | 64KB body |
Decision tree
Will your op finish in ≤ 30s?
├── yes → use ctx.http with the default 30s timeout. Done.
└── no
├── Single HTTP call that needs 30–180s?
│ └── yes → ctx.http.post(url, ..., timeout=120). Chat blocks until done.
└── Op > 180s OR you want chat to stay free?
→ handler returns an ack synchronously and calls
ctx.background_task(coro, long_running=...)
(the coro runs detached; its result is delivered to chat later)What changes for your user
Pattern A — ctx.http(timeout=N) (30–180s)
The user sees the chat "thinking" indicator for the duration of the call. Once the handler returns, the final ActionResult lands as a single bot turn. No additional setup.
background= / long_running= on @chat.function are advisory only
The background= and long_running= keyword arguments on @chat.function are advisory per-tool metadata — they are recorded in your manifest but the platform does not consume them to change runtime behavior. There is no decorator-level "auto-wrap" that detaches a handler. To actually run work in the background, call ctx.background_task(coro, long_running=...) explicitly inside your handler (Pattern B below).
Pattern B — ctx.background_task(coro) (v4.2.12+)
Your handler returns an acknowledgement synchronously, then hands the slow work to ctx.background_task(coro, long_running=...). The coroutine runs detached; its ActionResult is delivered to chat when it finishes. The user sees two bot turns:
- Immediate — the ack
ActionResultyour handler returns. Chat unblocks instantly. - Later — when the background coroutine returns, the platform injects a fresh bot turn rendering its
ActionResult. The user may have continued chatting about other things in between; the result lands in-place anyway.
@chat.function(
"refine_output",
description="Refine the given text via AI completion.",
action_type="write",
event="text_refined",
)
async def refine_output(ctx, params: RefineParams) -> ActionResult:
async def _work() -> ActionResult:
# Runs detached. This ActionResult is what the user sees later.
await ctx.progress(50, "Generating with AI")
resp = await ctx.http.post(api_url, json={...}, timeout=120)
return ActionResult.success(
summary="Refined output ready!",
data={"text": resp.body["text"]},
)
ctx.background_task(_work(), long_running=False)
return ActionResult.success(summary="Refining your text in the background…")Use the explicit form when you need a custom acknowledgement summary in turn 1, when your handler chooses synchronously vs. background at runtime based on parameters, or when your handler does some sync work first then detaches the slow tail.
See the recipe for full ctx.background_task code examples.
The user can close the browser between turn 1 and turn 2; the result lands in chat history and shows up on next open.
Federal contract — what the platform guarantees
Five long-running guarantees enforced for you
- HTTP timeout cap —
ctx.http(timeout=N)is capped at 180 seconds. A larger value raisesValueError. - Background result type — the coroutine you pass to
ctx.background_task()must returnActionResult. Returning anything else is logged as a critical event and the user receives a fallback error message instead. - Background task is user-scoped — every background task is bound to your extension and the requesting user at creation. Any attempt to cancel or check the status of another user's task is rejected with a 403.
- Chat delivery is user-scoped —
ctx.deliver_chat_message()and the automatic delivery from background tasks always land in the owning user's chat only. Any attempt to inject into another user's chat is rejected with a 403. - Every injection is audited — every chat injection is recorded in the audit trail. This is enforced by the platform, not left to the extension.
Progress emissions
For background tasks longer than a minute, emit ctx.progress(percent, message) at every coarse milestone. Two reasons:
- User feedback — Imperal renders a progress indicator in chat for the running task; users see what stage you're in.
- Liveness — the platform uses progress emissions as heartbeats. Long silence may cause the task to be reclaimed.
await ctx.progress(15, "Fetching context")
# ... work ...
await ctx.progress(50, "Generating with AI")
# ... work ...
await ctx.progress(90, "Saving")If the user cancels the task, the next ctx.progress() call raises TaskCancelled. Let it propagate — the platform handles the cancellation delivery to chat.
When NOT to use a background task
- Reads under 1 second. The orchestration overhead isn't worth it. Just return
ActionResultsynchronously. - CPU-bound loops with no
await. The platform's liveness check assumes the coroutine yields. Wrap heavy CPU work withawait asyncio.sleep(0)between iterations, or move it to an external service called viactx.http. - Streaming partial output to chat.
ctx.background_taskdelivers a single final result. For per-token streaming, build streaming directly into your handler instead.
Cross-links
Secrets
@ext.secret — store and use credentials safely: per-user keys and OAuth tokens (scope=user) AND your own shared developer-owned credentials like OAuth client secrets (scope=app), encrypted at rest, never exposed to admins, logs, or backups.
Local dev setup
Local dev setup for Imperal Cloud extensions: scaffold the file layout, write a chat function, and validate and test on your machine with no platform running.