Imperal Docs
Core Concepts

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

SurfaceWhen to reach for itCap
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:

  1. Immediate — the ack ActionResult your handler returns. Chat unblocks instantly.
  2. 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 capctx.http(timeout=N) is capped at 180 seconds. A larger value raises ValueError.
  • Background result type — the coroutine you pass to ctx.background_task() must return ActionResult. 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-scopedctx.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:

  1. User feedback — Imperal renders a progress indicator in chat for the running task; users see what stage you're in.
  2. 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 ActionResult synchronously.
  • CPU-bound loops with no await. The platform's liveness check assumes the coroutine yields. Wrap heavy CPU work with await asyncio.sleep(0) between iterations, or move it to an external service called via ctx.http.
  • Streaming partial output to chat. ctx.background_task delivers a single final result. For per-token streaming, build streaming directly into your handler instead.

On this page