Imperal Docs
Core Concepts

Long-running operations

How to handle ops longer than the 30s ctx.http default — federal cap, background tasks, chat injection

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 with a declarative-sugar form added in v4.2.13.

This page is the mental model. For copy-paste patterns, see Long-running AI calls.


The surfaces

SurfaceWhen to reach for itFederal cap
ctx.http(..., timeout=N)A single HTTP call legitimately needs 30–180 seconds.180s (I-LONGRUN-HTTP-CAP-180S)
@chat.function(background=True, long_running=False) (sugar, v4.2.13+)The whole handler body is the long work. SDK auto-wraps in ctx.background_task(); you write one handler, no inner coro.180s default; 1800s with long_running=True
ctx.background_task(coro, long_running=False) (explicit, v4.2.12+)You need a custom acknowledgement summary, conditional dispatch (sync vs background at runtime), or mixed sync + background in one handler.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?
            ├── Entire handler body is the long work + auto ack is fine
            │     → @chat.function(background=True, long_running=...)  ← SUGAR
            └── Need custom ack summary or conditional dispatch?
                  → handler returns ack synchronously, calls
                    ctx.background_task(coro, long_running=...)         ← EXPLICIT

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.

Pattern B (sugar) — @chat.function(background=True) (v4.2.13+)

Add background=True to the decorator and write a single handler body. The SDK auto-wraps the call in ctx.background_task() under the hood. The user sees two bot turns:

  1. Immediate (auto-generated) — "Started 'your_function_name' in background — the result will be sent to chat when it finishes." Chat unblocks instantly.
  2. Later — when your handler returns, the platform injects a fresh bot turn rendering the returned 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",
    background=True,
    long_running=False,
)
async def refine_output(ctx, params: RefineParams) -> ActionResult:
    # Body runs detached. The ActionResult below 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"]},
    )

Pattern C (explicit) — ctx.background_task(coro) (v4.2.12+)

Choose this over the sugar form when:

  • You need a custom acknowledgement summary in turn 1 instead of the auto-generated one.
  • Your handler chooses synchronously vs. background at runtime based on parameters.
  • Your handler does some sync work first, then detaches the slow tail.

See the recipe for both Pattern B (sugar) and Pattern C (explicit) full code examples.

In both patterns 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 LONGRUN invariants enforced for you

  • I-LONGRUN-HTTP-CAP-180Sctx.http(timeout=N) capped at 180 seconds. Larger raises ValueError pointing back here.
  • I-LONGRUN-BG-CORO-RETURNS-ACTIONRESULT — the coroutine you pass to ctx.background_task() MUST return ActionResult. Returning anything else triggers a critical audit row and delivers a fallback error message to the user.
  • I-LONGRUN-BG-USER-SCOPED — every background task is bound to (your_ext, user) at creation. Cross-user cancel/status access returns 403.
  • I-LONGRUN-CHAT-INJECT-USER-SCOPEDctx.deliver_chat_message() and the auto-delivery from background tasks both land in the message's owner user only. Cross-user inject returns 403.
  • I-LONGRUN-CHAT-INJECT-AUDIT-EVERY — every chat injection writes an audit row at the chokepoint. Not an extension policy — a platform-enforced contract.

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