Forge LLMs is available through Forge's Early Access Program (EAP). EAP grants selected users early testing access for feedback; APIs and features in EAP are experimental, unsupported, subject to change without notice, and not recommended for production — sign up here to participate.
For more details, see Forge EAP, Preview, and GA.
When building Forge apps with large language models (LLMs), some prompts or agentic workflows may exceed the default function timeout (55s). To handle these, offload work to a queue consumer (up to 15 minutes) and stream results back to the user interface (UI) using Realtime publish/subscribe. This pattern avoids storage-polling latency, reduces client/server round-trips, and keeps your macro responsive.
A queue consumer lets you run for longer (15 minutes) without blocking the initial invocation; Realtime keeps users informed the moment results (or errors) are available.
Important: You must use tokens with unique custom claims to secure your Realtime channels. Both the subscriber (frontend) and publisher (consumer) must create tokens using the same unique signature (custom claims) that include identifying information (such as channelName, accountId, or other unique identifiers). This is essential to:
When using Realtime in async functions (such as queue consumers, triggers, or scheduled functions), you must use subscribeGlobal() and publishGlobal() because the context-based methods (subscribe() and publish()) are not supported in async execution contexts.
Read more about securing Realtime channels with tokens.
Before you begin, you should be familiar with:
Flow Sequence
1 2UI subscribes → resolver enqueues → consumer runs LLM → publishes → UI renders
The final project structure after completing the walkthrough:
1 2<project-directory> ├── manifest.yml ├── package-lock.json ├── package.json ├── README.md └── src ├── frontend │ └── index.jsx ├── consumers │ └── index.js ├── resolvers │ └── index.js ├── index.js
For the complete runnable example, see the Source code section below.
Define modules in manifest.yml for the llm, queue consumer (extended timeout), macro UI, and function handlers (resolver and consumer).
1 2modules: llm: - key: llm-with-realtime-app model: - claude consumer: - key: llm-prompt-consumer queue: llm-prompt-consumer-queue function: consumer macro: - key: llm-with-forge-realtime-hello-world-macro resource: main render: native resolver: function: resolver title: llm-with-forge-realtime function: - key: resolver handler: index.resolverHandler - key: consumer handler: index.consumerHandler timeoutSeconds: 900 # 15 minutes (queue consumer max)
Ensure your package.json includes the necessary Forge SDK packages, for example:
1 2{ // ... "dependencies": { // ... "@forge/bridge": "5.8.0", "@forge/events": "^2.0.10", "@forge/llm": "^0.3.0", "@forge/realtime": "^0.3.0", } }
1 2//<project-directory>/src/frontend/index.jsx import { invoke, realtime } from '@forge/bridge'; const CHANNEL_NAME = "my-llm-realtime-channel"; const [token, setToken] = useState(null); const [customClaims, setCustomClaims] = useState({}); // ... other state variables like message, result, etc. // Request token with custom claims useEffect(() => { const fetchToken = async () => { const { token, customClaims } = await invoke('buildToken', { channelName: CHANNEL_NAME }); setToken(token); setCustomClaims(customClaims); }; fetchToken(); }, []); // Subscribe with token useEffect(() => { if (!token) return; let subscription; realtime.subscribeGlobal( CHANNEL_NAME, (payload) => setResult(payload), { token } ).then(sub => { subscription = sub; }); return () => { if (subscription) subscription.unsubscribe(); }; }, [token]); const handleSubmit = () => { setResult(null); invoke("sendLLMPrompt", { customClaims, prompt: { model: 'claude-haiku-4-5-20251001', messages: [{ role: "user", content: message.trim() }] } }); };
Token-based security is required: Both the subscriber (frontend) and publisher (consumer) must create tokens using the same custom claims. See Important security and technical requirements for details.
The resolver provides two functions:
buildToken: Critical security function - Creates a signed token using custom claims (such as channel name and account ID) to secure Realtime communication and ensure channel isolation. This token must be created with the same custom claims that the consumer will use when publishing. See Important security and technical requirements.sendLLMPrompt: Pushes the prompt payload to the queue for asynchronous processing.1 2//<project-directory>/src/resolvers/index.js import Resolver from '@forge/resolver'; import { Queue } from '@forge/events'; import { signRealtimeToken } from '@forge/realtime'; const resolver = new Resolver(); const queue = new Queue({ key: "llm-prompt-consumer-queue" }); resolver.define('buildToken', async ({ payload, context }) => { const customClaims = { channelName: payload.channelName, accountId: context.accountId, }; const { token } = await signRealtimeToken(payload.channelName, customClaims); return { token, customClaims }; }); resolver.define("sendLLMPrompt", async ({ payload }) => { await queue.push([{ body: payload }]); }); export const handler = resolver.getDefinitions();
Handle the long-running process for the LLM app. The consumer must sign a token using the same custom claims as the subscriber, then publish the result to the Realtime channel.
1 2//<project-directory>/src/consumers/index.js import { publishGlobal, signRealtimeToken } from '@forge/realtime'; import { chat } from '@forge/llm'; export const handler = async (event) => { const { customClaims, prompt } = event.body; const channelName = customClaims.channelName; let result; try { const chatResult = await chat(prompt); result = { status: "done", ...chatResult }; } catch (err) { result = { status: "error", error: err?.context?.responseText || "Unknown error occurred" }; } // Sign token with same custom claims as subscriber const { token } = await signRealtimeToken(channelName, customClaims); await publishGlobal( channelName, result, { token } ); };
Ensure you export the handlers for both the consumer and resolver functions.
1 2// <project-directory>/index.js import { handler as consumerHandler } from './consumers/index.js'; import { handler as resolverHandler } from './resolvers/index.js';
After completing the steps, deploy and then install the app into your site or environment.
1 2forge deploy forge install
When successful, your app will now handle long-running LLM prompts via the queue consumer and stream results back to the UI using Realtime.
Find the complete code for this tutorial in the llm-with-forge-realtime Bitbucket repository
Required security measures:
accountId or session-specific data in your custom claims.See Important security and technical requirements for implementation details.
subscribeGlobal() and publishGlobal() instead of subscribe() and publish().With an async function, a queue consumer, and Forge Realtime, your Forge LLMs app can run long-running prompts asynchronously and stream results to the UI, keeping it responsive and eliminating polling.
Rate this page: