Answers you can trust, from Codeables
Every page on Codeables is structured and verified — built so people and the AI agents they rely on can trust it. Explore more from the source behind this answer.
Explore CodeablesInngest quickstart: create my first background job triggered by an event
You’re here because you want to run a reliable background job every time something happens in your app—without standing up workers, wiring queues, or hand-rolling retries. This quickstart walks through creating your first event-triggered background job with Inngest, from npm init to seeing a successful run in Traces.
We’ll:
- Install and run the Inngest dev server locally
- Define an event-driven background job using
inngest.createFunction()andstep.run() - Trigger it from your app via the Events API
- Inspect the run (inputs, outputs, retries) in Traces
By the end, you’ll have a durable, observable background job that resumes from the last successful step instead of starting over on failure.
Prerequisites
You’ll get the smoothest path if you have:
- Node.js 18+
- npm, pnpm, or yarn
- A basic TypeScript or JavaScript project (or an empty folder to start fresh)
We’ll use TypeScript in examples, but the same structure works with plain JS.
Step 1: Initialize a project and install Inngest
From a new or existing Node project:
mkdir inngest-quickstart && cd inngest-quickstart
npm init -y
# Install the Inngest SDK + dev server
npm install inngest
npm install --save-dev inngest-cli
Add a simple tsconfig.json if you don’t already have one:
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"moduleResolution": "Node",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "dist"
},
"include": ["src"]
}
Create a src folder to hold your code:
mkdir src
Step 2: Start the Inngest dev server
The dev server gives you everything you need locally:
- An execution environment for your background jobs
- A local events ingestor
- Traces UI to inspect runs, logs, and step inputs/outputs
Run:
npx --ignore-scripts=false inngest-cli dev
This will:
- Start the dev server (usually on
http://127.0.0.1:8288) - Show you a local UI with Traces and function registration status
- Print a Dev Server URL and Ingestion URL you’ll use shortly
Keep this dev server running in a terminal tab—it’s your local control plane while you develop.
Step 3: Define your first Inngest client
Create src/inngest.ts:
import { Inngest } from "inngest";
export const inngest = new Inngest({
// This identifies your app in Inngest
name: "quickstart-app",
});
This inngest client will be used to:
- Define background jobs via
inngest.createFunction() - Register them with the dev server
- Connect events to your code
Step 4: Create a background job triggered by an event
Let’s build a simple background job that runs whenever a user signs up—for example, sending a welcome email or kicking off onboarding tasks.
Create src/functions/userSignedUp.ts:
import { inngest } from "../inngest";
export const userSignedUpJob = inngest.createFunction(
{ id: "user-signed-up-job" },
// Event name that will trigger this job
{ event: "app/user.signed_up" },
async ({ event, step }) => {
// Step 1: Log that we received the event
await step.run("log-received", async () => {
console.log("Received user signup event:", event.data);
});
// Step 2: Simulate sending a welcome email
const emailResult = await step.run("send-welcome-email", async () => {
const email = event.data.email as string;
// Replace this with your own email service call
console.log(`Sending welcome email to ${email}`);
// Simulate work
await new Promise((resolve) => setTimeout(resolve, 500));
return { status: "sent", email };
});
// Step 3: Return a final result (visible in Traces)
return {
message: "User signup job completed",
emailStatus: emailResult,
};
}
);
A few key things are happening here:
inngest.createFunction({ id }, { event }, handler)defines a durable background job tied to an event name.- Each
step.run("name", async () => ...)is a code-level transaction:- Automatically retried on failure
- Checkpointed on success
- Ensures the workflow resumes from the last successful step, not from the beginning
- The
eventobject carries whatever data you send when you emitapp/user.signed_up.
This is the core Inngest pattern: wrap your business logic in Steps, and let the platform handle retries and durability.
Step 5: Register the background job with your runtime
You now need to expose your userSignedUpJob to the dev server. How you do this depends on your HTTP framework (Next.js, Express, etc.). For a minimal Node HTTP server, here’s a simple pattern.
Install a light HTTP server (or use your own):
npm install express
Create src/server.ts:
import express from "express";
import { serve } from "inngest/express"; // HTTP adapter
import { inngest } from "./inngest";
import { userSignedUpJob } from "./functions/userSignedUp";
const app = express();
// Collect all functions you want to register
const functions = [userSignedUpJob];
// Expose the Inngest handler for the dev server
app.use(
"/api/inngest",
serve({ inngest, functions })
);
const PORT = 3000;
app.listen(PORT, () => {
console.log(`App server listening on http://localhost:${PORT}`);
console.log(`Inngest handler at http://localhost:${PORT}/api/inngest`);
});
This serve({ inngest, functions }) call wires your functions into an HTTP endpoint that Inngest will call to:
- Discover which functions exist
- Execute them when events match
- Report traces and step-level logs back to the dev server
Add a dev script in package.json to run this server:
{
"scripts": {
"dev": "ts-node src/server.ts"
}
}
Then run your app server in another terminal:
npm run dev
You should now have:
- Inngest dev server running (
inngest-cli dev) - Your app server running with the Inngest handler at
/api/inngest
The dev server UI should show that your function user-signed-up-job is registered.
Step 6: Connect your local app server to the dev server
When you ran npx --ignore-scripts=false inngest-cli dev, you should have seen a prompt (or output) to configure your framework or handler URL.
If you need to specify it manually, open the dev server UI in your browser (typically printed as something like):
Dev server UI: http://127.0.0.1:8288
In the UI, configure your App URL (or “Framework URL”) to point to your local handler:
http://localhost:3000/api/inngest
Once connected, the dev server will:
- Discover
userSignedUpJob - Keep the function synced as you edit and save
- Route events to your function and stream results into Traces
Step 7: Trigger the background job by sending an event
The function is listening for app/user.signed_up. Let’s send that event using the Inngest Events API.
With the dev server running, find the Ingestion URL in the CLI output or in the UI. It will look something like:
http://127.0.0.1:8288/api/v1/events
Use curl to emit an event:
curl -X POST "http://127.0.0.1:8288/api/v1/events" \
-H "Content-Type: application/json" \
-d '{
"name": "app/user.signed_up",
"data": {
"userId": "user_123",
"email": "user@example.com",
"plan": "pro"
}
}'
You should receive a response confirming the event was accepted. Behind the scenes:
- The dev server ingests the event
- It matches the event name to your
userSignedUpJobfunction - It executes your Steps, applying retries and checkpointing as needed
Step 8: Inspect the run in Traces
Open the dev server UI (e.g. http://127.0.0.1:8288) and navigate to Traces:
- You’ll see a list of recent function runs
- Click the run for
user-signed-up-job
Inside the trace view you can:
- See each
step.run()as a named step (log-received,send-welcome-email) - Inspect inputs/outputs for that step
- View structured logs and durations
- Confirm the final result your handler returned
If something fails, you’ll see:
- Which step failed
- The error and stack trace
- Retry attempts and backoff behavior
This is where Inngest removes the normal background job pain: no more hunting across log streams or guessing which part of a multi-step flow ran before a timeout.
Step 9: Add a failure and watch automatic retries
To see durability in action, temporarily throw an error in one of your steps.
Update src/functions/userSignedUp.ts:
export const userSignedUpJob = inngest.createFunction(
{ id: "user-signed-up-job" },
{ event: "app/user.signed_up" },
async ({ event, step }) => {
await step.run("log-received", async () => {
console.log("Received user signup event:", event.data);
});
const emailResult = await step.run("send-welcome-email", async () => {
const email = event.data.email as string;
// Force a failure for certain users
if (email === "fail@example.com") {
throw new Error("Simulated email provider outage");
}
console.log(`Sending welcome email to ${email}`);
await new Promise((resolve) => setTimeout(resolve, 500));
return { status: "sent", email };
});
return {
message: "User signup job completed",
emailStatus: emailResult,
};
}
);
Restart your dev server if needed, then trigger another event:
curl -X POST "http://127.0.0.1:8288/api/v1/events" \
-H "Content-Type: application/json" \
-d '{
"name": "app/user.signed_up",
"data": {
"userId": "user_456",
"email": "fail@example.com",
"plan": "free"
}
}'
Now in Traces you’ll see:
log-receivedsucceeds and is checkpointedsend-welcome-emailfails, is retried according to the function’s retry policy- The workflow resumes from
send-welcome-email, not fromlog-received
That’s the core durability model: each Step is a code-level transaction with automatic retries and once-on-success semantics.
Step 10: Wire events from your real app
So far you’ve used curl to emit events. In a real app, you’ll typically:
- Emit events from API handlers, webhooks, or your frontend
- Use Inngest’s SDKs or direct HTTP calls to the Events API
For a Node backend, you can call the Events API directly when something happens, for example inside your signup route:
import axios from "axios";
const INNGEST_INGEST_URL = "http://127.0.0.1:8288/api/v1/events"; // dev
async function onUserSignedUp(user: { id: string; email: string; plan: string }) {
await axios.post(INNGEST_INGEST_URL, {
name: "app/user.signed_up",
data: {
userId: user.id,
email: user.email,
plan: user.plan,
},
});
}
When you move to Inngest Cloud, you’ll:
- Replace the ingestion URL with your cloud Events URL
- Keep your function code (
inngest.createFunction,step.run) the same - Run in your preferred environment (edge, serverless, or traditional) triggered by API calls, webhooks, or schedules
No workers, queues, or cron setup—just events and Steps.
Where to go next
You now have a durable, event-triggered background job that:
- Runs whenever you emit
app/user.signed_up - Breaks logic into named Steps with automatic retries and checkpointing
- Surfaces full step-level visibility via Traces
From here, the next useful things to explore are:
- Multi-step workflows: Chain more
step.run()calls and see how checkpointing behaves on partial failure. - Flow Control: Add multi-tenant concurrency keys or throttling to protect shared resources and avoid noisy neighbors.
- Replay: Use Replay in Inngest Cloud to re-run failed or previously successful jobs in bulk without building admin tooling.
- Durable endpoints: Turn API handlers and webhook receivers into durable endpoints that survive timeouts and provider flakiness.
When you’re ready to run this in production and connect it to Inngest Cloud, the team can help you wire up environments, metrics, and exports (e.g. Prometheus, Datadog) so you can query, cancel, or replay runs across your stack.