| name | inngest-function-template |
| description | Create reliable Inngest functions with dual triggers, step checkpoints, and failure handling |
| allowed-tools | ["Read","Write","Edit","Glob","Grep"] |
Inngest Function Template
Create production-ready Inngest functions following CircleTel patterns.
When to Use
- "Create an Inngest function for X"
- "Add background job for X"
- "Migrate cron job to Inngest"
- "Add scheduled sync for X"
Step 1: Define Event Types
Add to lib/inngest/client.ts:
'[feature]/[action].requested': {
data: {
triggered_by: 'cron' | 'manual';
log_id?: string;
options?: { dryRun?: boolean };
};
};
'[feature]/[action].completed': {
data: {
log_id: string;
stats: { processed: number; errors: number };
};
};
'[feature]/[action].failed': {
data: {
log_id: string;
error: string;
attempt: number;
};
};
Step 2: Create the Function
import { inngest } from '../client';
import { createClient } from '@/lib/supabase/server';
export const [feature][Action]Function = inngest.createFunction(
{
id: '[feature]-[action]',
name: '[Feature] [Action]',
retries: 3,
cancelOn: [{ event: '[feature]/[action].cancelled', match: 'data.log_id' }],
},
[
{ cron: '0 22 * * *' },
{ event: '[feature]/[action].requested' },
],
async ({ event, step }) => {
const supabase = await createClient();
const isCronTrigger = !event?.data?.log_id;
const logId = event?.data?.log_id;
try {
await step.run('initialize', async () => {
if (logId) {
await supabase
.from('[feature]_logs')
.update({ status: 'running', started_at: new Date().toISOString() })
.eq('id', logId);
}
});
const data = await step.run('fetch-data', async () => {
return fetchedData;
});
const batches = chunkArray(data, 50);
for (let i = 0; i < batches.length; i++) {
await step.run(`process-batch-${i}`, async () => {
});
}
await step.run('send-completion-event', async () => {
await inngest.send({
name: '[feature]/[action].completed',
data: { log_id: logId, stats: { processed: data.length, errors: 0 } },
});
});
return { success: true, processed: data.length };
} catch (error) {
await step.run('send-failure-event', async () => {
await inngest.send({
name: '[feature]/[action].failed',
data: {
log_id: logId,
error: error instanceof Error ? error.message : 'Unknown error',
attempt: 1,
},
});
});
throw error;
}
}
);
Step 3: Register the Function
Add to lib/inngest/index.ts:
import { [feature][Action]Function } from './functions/[feature]-[action]';
export const functions = [
[feature][Action]Function,
];
Step 4: Create Manual Trigger API
export async function POST(request: NextRequest) {
const supabase = await createClient();
const { data: running } = await supabase
.from('[feature]_logs')
.select('id')
.in('status', ['pending', 'running'])
.limit(1)
.single();
if (running) {
return NextResponse.json(
{ success: false, error: 'Already in progress', log_id: running.id },
{ status: 409 }
);
}
const { data: log } = await supabase
.from('[feature]_logs')
.insert({ status: 'pending', trigger_type: 'manual', triggered_by: user.id })
.select('id')
.single();
await inngest.send({
name: '[feature]/[action].requested',
data: { triggered_by: 'manual', log_id: log.id },
});
return NextResponse.json({ success: true, log_id: log.id });
}
Step 5: Create Status API
export async function GET() {
const supabase = await createClient();
const { data } = await supabase
.from('[feature]_logs')
.select('*')
.order('created_at', { ascending: false })
.limit(1)
.single();
return NextResponse.json(data);
}
Step 6: Add UI Polling
const triggerAction = async () => {
setStatus('pending');
const response = await fetch('/api/admin/[feature]/[action]', { method: 'POST' });
const data = await response.json();
if (data.success) {
const pollInterval = setInterval(async () => {
const statusRes = await fetch('/api/admin/[feature]/[action]/status');
const status = await statusRes.json();
if (status.status === 'completed' || status.status === 'failed') {
clearInterval(pollInterval);
if (status.status === 'completed') refreshData();
}
}, 2000);
}
};
Checklist
Parallel Provider Checks Pattern
For functions that check multiple external APIs:
const [result1, result2, result3] = await Promise.allSettled([
checkProvider1(coords),
checkProvider2(coords),
checkProvider3(coords),
]);
const services = Array.isArray(provider?.services) ? provider.services : [];
const isAvailable = services.some(
(s: { type?: string }) => s?.type === 'target'
);
Related Files
lib/inngest/client.ts - Event types
lib/inngest/functions/ - Function implementations
lib/inngest/index.ts - Function registry
supabase/migrations/ - Logs tables