Use waitUntil for work the caller does not need to wait on, such as analytics, notifications, or cleanup. The response returns immediately while the runtime tracks the work until it finishes.
The Pattern
waitUntil accepts a promise or callback. The callback starts right away, runs independently of the response, and multiple callbacks can run at the same time.
This example uses @agentuity/schema for lightweight input and output validation:
typescriptsrc/agent/order-processor/agent.ts
import { createAgent } from '@agentuity/runtime';
import { s } from '@agentuity/schema';
const agent = createAgent('OrderProcessor', {
schema: {
input: s.object({
orderId: s.string(),
userId: s.string(),
}),
output: s.object({
status: s.literal('confirmed'),
orderId: s.string(),
}),
},
handler: async (ctx, input) => {
const { orderId, userId } = input;
// Process the order synchronously
const order = await processOrder(orderId);
// Background: send confirmation email
ctx.waitUntil(async () => {
await sendConfirmationEmail(userId, order);
ctx.logger.info('Confirmation email sent', { orderId });
});
// Background: update analytics
ctx.waitUntil(async () => {
await trackPurchase(userId, order);
});
// Background: notify warehouse
ctx.waitUntil(async () => {
await notifyWarehouse(order);
});
// Response returns immediately, background tasks continue
return {
status: 'confirmed',
orderId,
};
},
});
export default agent;With Durable Streams
Create a stream for the client to poll, then populate it in the background:
typescriptsrc/agent/async-generator/agent.ts
import { createAgent } from '@agentuity/runtime';
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
import { s } from '@agentuity/schema';
const agent = createAgent('AsyncGenerator', {
schema: {
input: s.object({ prompt: s.string() }),
output: s.object({
streamId: s.string(),
streamUrl: s.string(),
}),
},
handler: async (ctx, input) => {
// Create a durable stream the client can read from
const stream = await ctx.stream.create('generation', {
contentType: 'text/plain',
metadata: { sessionId: ctx.sessionId },
});
// Generate content in the background
ctx.waitUntil(async () => {
try {
const { textStream } = streamText({
model: openai('gpt-5-mini'),
prompt: input.prompt,
});
for await (const chunk of textStream) {
await stream.write(chunk);
}
} finally {
await stream.close();
}
});
// Return stream URL immediately
return {
streamId: stream.id,
streamUrl: stream.url,
};
},
});
export default agent;Progress Reporting
Write progress updates to a stream as background work proceeds:
typescriptsrc/agent/batch-processor/agent.ts
import { createAgent } from '@agentuity/runtime';
import { s } from '@agentuity/schema';
const agent = createAgent('BatchProcessor', {
schema: {
input: s.object({ items: s.array(s.string()) }),
output: s.object({ progressUrl: s.string() }),
},
handler: async (ctx, input) => {
const progress = await ctx.stream.create('progress', {
contentType: 'application/x-ndjson',
});
ctx.waitUntil(async () => {
try {
for (const [index, item] of input.items.entries()) {
await processItem(item);
await progress.write(JSON.stringify({
completed: index + 1,
total: input.items.length,
percent: Math.round(((index + 1) / input.items.length) * 100),
}) + '\n');
}
await progress.write(JSON.stringify({ done: true }) + '\n');
} finally {
await progress.close();
}
});
return { progressUrl: progress.url };
},
});
export default agent;Key Points
- Non-blocking: Response returns immediately while tracked work continues
- Concurrent: Multiple
waitUntilcallbacks run at the same time - Error isolation: Background task failures don't affect the response
- Always close streams: Use
finallyblocks to ensure cleanup
See Also
- Durable Streams for stream creation and management
- Webhook Handler for another
waitUntilexample