Skip to main content

Building Webhook-Enabled LLM Workflows in JavaScript with Codehooks.io

· 20 min read
Martin
Co-Founder and Maker @ Codehooks

Most AI projects don't need a fleet of orchestration tools to run a few prompt chains. The real work is state management, retries, scheduling, and simple persistence—the operational glue between LLM API calls.

This post shows you how to build a production-ready text summarization workflow using Codehooks.io, the Workflow API, and OpenAI. You'll learn:

  • OpenAI API integration patterns: Error handling, retries, rate limiting, and cost optimization
  • Workflow state management: Building reliable multi-step processes with caching and persistence
  • Webhook triggers: Event-driven workflows that respond to GitHub issues (and other external services)
  • Programmatic access: How to trigger and manage workflows via REST API, CLI, and webhooks

By the end, you'll have a working summarizer that caches results, stores them in a NoSQL database, and can be triggered via REST API or GitHub webhooks—all deployed with a single command.

LLM workflow architecture

The messy reality of LLM pipelines (the problems)

If you've built more than one "simple" AI script, you've likely met these issues:

  1. State between steps – You call an LLM, call another API, then need to combine/compare results. Where do you keep ephemeral state? How do you re-run a step without duplicating side effects?
  2. Retries and idempotency – A single transient 500 from a model provider shouldn't break the whole chain or duplicate writes.
  3. OpenAI API integration complexity – Rate limits, token counting, model selection, error handling, and cost management add layers of complexity beyond simple HTTP calls.
  4. Scheduling and triggers – Many pipelines aren't one-shot. They run hourly, daily, or react to external events via webhooks from services like GitHub, Slack, or Stripe.
  5. Persistence – You need a place to store inputs/outputs, cache expensive calls, and expose results to other services.
  6. Operational sprawl – A script becomes a service; a service becomes a DAG; suddenly you're maintaining queues, workers, YAML, and dashboards just to run a few prompts.
  7. Language/tool drift – Frameworks like LangChain, LlamaIndex, Haystack, or workflow engines like Airflow/Prefect/Celery are powerful, but they assume extra infrastructure and a Python-first stack. That’s perfect for some teams, but overkill for many web and product engineers who just want reliable workflows in JavaScript.

These are engineering problems, not “AI problems.” The question is: what’s the lightest reliable runtime that gives you state, retries, scheduling, and storage — without spinning up a new platform?


The middle layer we actually want

Between prompt-chaining libraries and heavyweight workflow engines, there’s a practical middle:

  • Functions as steps, with a small state machine to move between them
  • Built-in storage for docs and key–value cache
  • Scheduling (cron-like) and webhook triggers for external events
  • HTTP + webhook endpoints to start/inspect runs
  • One deploy

That’s the layer Codehooks provides. You write JavaScript; the platform handles routing, persistence, and workflow state. No extra orchestrator.

Useful links: Workflow API · ChatGPT backend API prompt


OpenAI API Integration Patterns

Before diving into examples, let's establish solid patterns for OpenAI API integration in production workflows.

Error Handling & Retries

A reusable helper with retries and exponential backoff:

const callOpenAIWithRetry = async (messages, options = {}, maxRetries = 3) => {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(
'https://api.openai.com/v1/chat/completions',
{
method: 'POST',
headers: {
'content-type': 'application/json',
authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
},
body: JSON.stringify({
model: options.model || 'gpt-4o-mini',
messages,
temperature: options.temperature ?? 0.3,
max_tokens: options.max_tokens ?? 300,
}),
}
);

if (response.status === 429) {
// Rate limit - exponential backoff and retry
const delay = Math.pow(2, attempt) * 1000;
if (attempt < maxRetries) {
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
}
}

if (!response.ok) {
const error = await response.text().catch(() => 'Unknown error');
throw new Error(`OpenAI API error ${response.status}: ${error}`);
}

const data = await response.json();
return data?.choices?.[0]?.message?.content?.trim() || '';
} catch (error) {
if (attempt === maxRetries) throw error;
// Wait before retry for network errors
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
}
}
};

Cost Optimization

  • Model selection: Use gpt-4o-mini for most tasks (significantly cheaper than GPT‑4 while still very capable)
  • Token management: Set appropriate max_tokens limits
  • Caching: Cache responses for identical inputs (shown in example below)
  • Batch processing: Group similar requests when possible

Rate Limiting

OpenAI rate limits depend on your account, model, and tier. They also change over time.

Best practices:

  • Treat HTTP 429 as a signal to back off and retry
  • Implement exponential backoff (as in the helper above)
  • Monitor limits and usage in the OpenAI dashboard
  • Prefer more efficient models (like gpt-4o-mini) where possible

Security

  • Store API keys as encrypted environment variables
  • Use different keys for different environments
  • Monitor usage and set spending limits in OpenAI dashboard

Example — Summarizer with cache and storage

We'll build a small workflow that accepts text, checks a cache, calls OpenAI, stores the result, and returns it. We’ll also add a webhook endpoint that can summarize GitHub issues and comments.

Setup

npm install -g codehooks
coho create aipipeline
cd aipipeline
npm install codehooks-js node-fetch
coho set-env OPENAI_API_KEY 'sk-******' --encrypted

# Optional: for webhook signature verification (recommended for production)
coho set-env GITHUB_WEBHOOK_SECRET 'your-webhook-secret' --encrypted

index.js

Replace the default index.js created by coho create with this workflow code (all in one file for simplicity):

import { app, Datastore } from 'codehooks-js';
import fetch from 'node-fetch';
import crypto from 'crypto';

// Reusable OpenAI helper with retries and exponential backoff
async function callOpenAIWithRetry(messages, options = {}, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(
'https://api.openai.com/v1/chat/completions',
{
method: 'POST',
headers: {
'content-type': 'application/json',
authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
},
body: JSON.stringify({
model: options.model || 'gpt-4o-mini',
messages,
temperature: options.temperature ?? 0.3,
max_tokens: options.max_tokens ?? 300,
}),
}
);

if (response.status === 429) {
// Rate limit - exponential backoff and retry
const delay = Math.pow(2, attempt) * 1000;
if (attempt < maxRetries) {
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
}
}

if (!response.ok) {
const error = await response.text().catch(() => 'Unknown error');
throw new Error(`OpenAI API error ${response.status}: ${error}`);
}

const data = await response.json();
return data?.choices?.[0]?.message?.content?.trim() || '';
} catch (error) {
if (attempt === maxRetries) throw error;
// Wait before retry for network or transient errors
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
}
}
}

// Helper function to verify GitHub webhook signature using raw body
function verifyGitHubSignature(rawBody, signatureWithPrefix, secret) {
if (!signatureWithPrefix || !secret) return false;
if (!signatureWithPrefix.startsWith('sha256=')) return false;

const signature = signatureWithPrefix.slice('sha256='.length);
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');

// Use timingSafeEqual to avoid timing attacks
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}

// Workflow: summarize text and store
const summarizeWorkflow = app.createWorkflow(
'summarize',
'Summarize text using OpenAI and store the result',
{
start: async (state, goto) => {
if (!state?.text) return goto(null, { error: 'Missing text input' });
goto('checkCache', { ...state, steps: ['start'] });
},

checkCache: async (state, goto) => {
const db = await Datastore.open();
// Use MD5 hash of entire text for proper cache key
const key = `summary:${crypto
.createHash('md5')
.update(state.text)
.digest('hex')}`;

const cached = await db.get(key);
const newState = {
...state,
steps: [...(state.steps || []), 'checkCache'],
};

// If cached, skip to finish without storing duplicate
if (cached) {
return goto('finish', {
...newState,
summary: cached,
cached: true,
});
}

goto('callOpenAI', newState);
},

callOpenAI: async (state, goto) => {
const newState = {
...state,
steps: [...(state.steps || []), 'callOpenAI'],
};

// OpenAI API call via the reusable helper
const summary = await callOpenAIWithRetry(
[
{ role: 'system', content: 'You are a helpful assistant.' },
{ role: 'user', content: `Summarize this:

${state.text}` },
],
{
model: 'gpt-4o-mini', // Cost-effective model for summarization
temperature: 0.3, // Lower temperature for consistent summaries
max_tokens: 300, // Limit tokens to control costs
}
);

goto('cacheAndStore', { ...newState, summary });
},

cacheAndStore: async (state, goto) => {
const db = await Datastore.open();

// Use MD5 hash for consistent cache key
const key = `summary:${crypto
.createHash('md5')
.update(state.text)
.digest('hex')}`;

// TTL is in milliseconds – 60 * 1000 = 60 seconds
await db.set(key, state.summary, { ttl: 60 * 1000 });

goto('store', {
...state,
steps: [...(state.steps || []), 'cacheAndStore'],
});
},

store: async (state, goto) => {
const db = await Datastore.open();
const doc = await db.insertOne('summaries', {
text: state.text,
summary: state.summary,
cached: !!state.cached,
source: state.source || 'api',
repository: state.repository || null,
createdAt: new Date().toISOString(),
});

goto('finish', {
...state,
...doc,
steps: [...(state.steps || []), 'store'],
});
},

finish: async (state, goto) => {
const steps = [...(state.steps || []), 'finish'];
console.log('Workflow finished');
console.log('WorkflowId:', state._id || 'N/A');
console.log('Steps executed:', steps.join(' → '));
goto(null, { ...state, steps });
},
}
);

// HTTP endpoints

// Direct API trigger for summarization
app.post('/summaries', async (req, res) => {
const result = await summarizeWorkflow.start({ text: req.body?.text });
res.json(result);
});

// Fetch stored summaries
app.get('/summaries', async (req, res) => {
const db = await Datastore.open();
const items = await db
.getMany('summaries', {}, { sort: { createdAt: -1 }, limit: 10 })
.toArray();
res.json(items);
});

// Webhook endpoint - trigger summarization from GitHub issues, comments, or generic payloads
app.post('/webhook/summarize', async (req, res) => {
// Verify webhook signature for security (GitHub-style HMAC)
if (process.env.GITHUB_WEBHOOK_SECRET) {
const signature = req.headers['x-hub-signature-256'];
const ok = verifyGitHubSignature(
req.rawBody,
signature,
process.env.GITHUB_WEBHOOK_SECRET
);

if (!ok) {
return res.status(401).json({ error: 'Invalid signature' });
}
}

// Extract text from GitHub issue webhook payload
// Supports: issue opened, issue comment, pull request, or generic text
let text = '';
let source = 'webhook';

if (req.body.issue) {
// GitHub issue or PR
text = `${req.body.issue.title}

${req.body.issue.body || ''}`;
source = `github-issue-${req.body.issue.number}`;
} else if (req.body.comment) {
// GitHub issue/PR comment
text = req.body.comment.body;
source = `github-comment-${req.body.comment.id}`;
} else {
// Generic fallback for other webhook providers
text = req.body?.content || req.body?.text || req.body?.message || '';
}

if (!text || text.trim().length === 0) {
return res
.status(400)
.json({ error: 'No text content in webhook payload' });
}

// Start workflow asynchronously
const result = await summarizeWorkflow.start({
text: text.trim(),
source,
repository: req.body.repository?.full_name || 'unknown',
});

// Return a lightweight response while the workflow runs in the background
res.json({ status: 'processing', workflowId: result._id });
});

export default app.init();

In Codehooks, req.rawBody contains the unparsed request body, which is required for correct webhook signature verification with providers like GitHub.

Deploy and get your endpoint

Now deploy your code and get your API endpoint:

$ coho deploy
Project: aipipeline-4cqe Space: dev
Deployed Codehook successfully! 🙌

Check your logs with: coho logs --follow

Finding your API endpoint and token

The coho info command shows your deployment details and if you add the --examples parameter it also provides example cURL commands. Here's what to look for:

$ coho info --examples

Project name: aipipeline-4cqe
Team: Amy Jones (personal account)

API endpoints:

https://funny-wizard-xyz9.codehooks.io
https://aipipeline-4cqe.api.codehooks.io/dev
https://api.example.com

Examples in cURL:

curl -H 'x-apikey: 73c53e83-c8b5-4b5f-8452-65ebca859ac1' -H 'Content-Type: application/json' https://funny-wizard-xyz9.codehooks.io/users

curl -H 'x-apikey: 73c53e83-c8b5-4b5f-8452-65ebca859ac1' -H 'Content-Type: application/json' --data-raw '{"name": "Mr. Code Hooks", "active": true }' --request POST https://funny-wizard-xyz9.codehooks.io/users

Spaces:

┌──────────────┬────────────────────────────────────────────┬───────────────┬──────┬────────────────────────────┐
│ Name │ Tokens │ Allowed Hosts │ Jwks │ Env │
├──────────────┼────────────────────────────────────────────┼───────────────┼──────┼────────────────────────────┤
│ dev (active) │ xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (RW) │ │ │ OPENAI_API_KEY=(encrypted) │
└──────────────┴────────────────────────────────────────────┴───────────────┴──────┴────────────────────────────┘

API Endpoint: Use any of the URLs listed. The first one is a unique random name for the space. The second one (https://aipipeline-4cqe.api.codehooks.io/dev) includes the project name and space name. The third one is a custom domain you can set up for your project.

API Token: Copy one of the tokens from the Tokens column in the Spaces table (the UUID values marked with RW for read-write access). Use this token as the value for the x-apikey header in your API calls.

Testing the workflow

You can test the workflow using cURL commands, a reusable shell script, or API clients like Bruno, Postman, or Insomnia.

Option 1: Quick tests with cURL

Replace the URL and token with your values from coho info:

# Start a summarization
curl -X POST https://aipipeline-4cqe.api.codehooks.io/dev/summaries \
-H "Content-Type: application/json" \
-H "x-apikey: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \
-d '{
"text": "Artificial intelligence is transforming software development. Modern AI tools help developers write code faster, debug more efficiently, and build smarter applications. From code completion to automated testing, AI is becoming an essential part of the developer toolkit."
}'

# Response example:
# {
# "_id": "abc123...",
# "text": "Artificial intelligence is transforming...",
# "summary": "AI tools are revolutionizing software development by enhancing code writing, debugging, and application intelligence.",
# "cached": false,
# "source": "api",
# "repository": null,
# "createdAt": "2025-11-15T10:30:00.000Z"
# }

# Fetch stored summaries
curl https://aipipeline-4cqe.api.codehooks.io/dev/summaries \
-H "x-apikey: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

# Trigger via webhook (simulating GitHub issue webhook)
curl -X POST https://aipipeline-4cqe.api.codehooks.io/dev/webhook/summarize \
-H "Content-Type: application/json" \
-d '{
"action": "opened",
"issue": {
"number": 123,
"title": "Application crashes on startup",
"body": "When I run the application, it immediately crashes with error code 500. This happens on both macOS and Linux."
},
"repository": {
"full_name": "myorg/myrepo"
}
}'

Option 2: API clients (Bruno, Postman, Insomnia)

For a more visual testing experience, use API clients. All three support importing cURL commands - just copy any of the cURL examples from Option 1 above and use the "Import from cURL" feature in your preferred tool.

Option 3: Reusable shell script

For easier repeated testing, create a summarize.sh script that handles text via argument, stdin, or uses sample text:

#!/bin/bash

PROJECTNAME=your-project-name
API_KEY="your-api-key"

# Check if text is provided as argument
if [ -n "$1" ]; then
TEXT="$1"
echo "Original text length: ${#TEXT} characters"
elif [ ! -t 0 ]; then
# Read from stdin if available (not a TTY)
TEXT=$(cat)
echo "Read from stdin, length: ${#TEXT} characters"
else
# Use a sample text
TEXT="Artificial intelligence is transforming software development..."
echo "Using sample text"
fi

# Escape the text for JSON
TEXT_ESCAPED=$(echo "$TEXT" | jq -Rs .)

curl -X POST https://${PROJECTNAME}.api.codehooks.io/dev/summaries \
-H "Content-Type: application/json" \
-H "x-apikey: $API_KEY" \
-d "{\"text\": $TEXT_ESCAPED}"

Usage examples:

# Make it executable
chmod +x summarize.sh

# Use sample text
./summarize.sh

# Provide text as argument
./summarize.sh "Your custom text here"

# Read from file via stdin
cat article.txt | ./summarize.sh
./summarize.sh < article.txt

# Test with long texts without truncation
echo "Very long text content..." | ./summarize.sh
Handling long texts

When testing with long texts, always use stdin (piping or redirection) rather than command-line arguments. Shell arguments have length limits that can truncate your text. The script automatically detects the input method and reports text length before sending.

Monitoring workflow execution

After running your workflows, you can inspect their execution status with live updates:

# One-time snapshot
coho workflow-status

# Live updates (recommended) - updates every 2 seconds
coho workflow-status --follow
Workflow observability with live updates

The coho workflow-status --follow command provides live monitoring of your workflows, updating every 2 seconds. This is invaluable for watching workflows execute in real-time:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Workflow Status (Live) - Press Ctrl+C to exit

Last updated: 20:22:08 | Next update in 2s

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Workflow Report
===============

Total workflow instances: 4
Unique steps: 6

Workflow Structure (6 steps):
1. start
2. checkCache
3. callOpenAI
4. cacheAndStore
5. store
6. finish

Current State Distribution:
start checkCache callOpenAI cacheAndStore store finish
------------ -------------- -------------- ----------------- ------------ ------------
1 0 1 0 0 2

What this tells you:

  • Live updates: See workflows progress through steps in real-time
  • Step sequence: The actual execution path through your workflow
  • Total instances: How many times the workflow has been triggered
  • State distribution: Where each workflow instance ended up (all 2 completed at finish in this example)
  • Debugging: If workflows get stuck, you'll see them stopped at intermediate steps
  • Cache verification: Watch the difference between cached (start → checkCache → finish) and uncached (full path) executions

This visibility is crucial for debugging production workflows and understanding execution patterns. Use --follow during development and testing to see your workflows in action!

Flow

Why this solves real problems

  • State lives in state and Datastore; steps remain small and testable.
  • Retries can be added per step (re-run from callOpenAI without duplicating store).
  • Scheduling is one line with app.job() if you want periodic summaries.
  • Webhooks let you react to external events like GitHub issues without polling.

Key implementation details

MD5 hash for cache keys: Using crypto.createHash('md5').update(state.text).digest('hex') creates a unique, consistent cache key based on the entire text content.

Proper cache flow: When a cached summary is found, the workflow goes directly to the finish step instead of creating a duplicate database record. This prevents storing the same summary multiple times while still allowing cache hits to avoid expensive OpenAI API calls. The TTL is set to 60 seconds, so the summary will be refreshed after 60 seconds. This is a trade-off between freshness and cost. You can change the TTL to your needs.

Finish step with tracking: Every workflow path ends at a finish step that logs:

  • The workflowId for debugging
  • The execution path taken (e.g., start → checkCache → finish for cached vs start → checkCache → callOpenAI → cacheAndStore → store → finish for uncached)

This makes it easy to verify caching is working and debug any issues.

Step tracking: Each step adds itself to a steps array in the state, providing full visibility into the workflow execution path. This is invaluable for debugging and understanding how workflows behave in production.

Extending to multi-step workflows

To add more LLM calls (e.g., classify → summarize → sentiment analysis), simply add more steps to the workflow. Each step receives the state from the previous step and can add new properties before calling goto():

classify: async (state, goto) => {
// Call OpenAI for classification
const category = await callOpenAIWithRetry(
[{ role: 'user', content: 'Classify this: ' + state.text }],
{ model: 'gpt-4o-mini' }
);

goto('summarize', {
...state,
category,
steps: [...(state.steps || []), 'classify'],
});
},

summarize: async (state, goto) => {
// Use category in the prompt
const summary = await callOpenAIWithRetry(
[
{
role: 'user',
content: `Summarize this ${state.category} text: ${state.text}`,
},
],
{ model: 'gpt-4o-mini' }
);

goto('sentiment', {
...state,
summary,
steps: [...(state.steps || []), 'summarize'],
});
},

sentiment: async (state, goto) => {
const sentiment = await callOpenAIWithRetry(
[{ role: 'user', content: 'Analyze sentiment: ' + state.text }],
{ model: 'gpt-4o-mini' }
);

goto('store', {
...state,
sentiment,
steps: [...(state.steps || []), 'sentiment'],
});
},

The pattern remains the same regardless of how many steps you chain together. Split steps into separate files for larger workflows.


Webhook-Triggered Workflows

Webhooks enable event-driven AI workflows. External services (GitHub, Slack, Stripe) can trigger workflows automatically when events occur — no polling required.

Key Benefits

  • Real-time processing: Trigger AI analysis immediately when data becomes available
  • Integration-friendly: Most SaaS platforms support outbound webhooks
  • Efficient: Event-driven beats constant polling

Example: GitHub Issue Triage (variation)

Here’s a variation on the previous webhook example, where a workflow triages newly opened issues. You can reuse the same verifyGitHubSignature helper from the main index.js example above.

app.post('/webhook/github', async (req, res) => {
// Verify signature using req.rawBody
if (process.env.GITHUB_WEBHOOK_SECRET) {
const signature = req.headers['x-hub-signature-256'];
const ok = verifyGitHubSignature(
req.rawBody,
signature,
process.env.GITHUB_WEBHOOK_SECRET
);

if (!ok) {
return res.status(401).json({ error: 'Invalid signature' });
}
}

const event = req.body;

if (event.action === 'opened' && event.issue) {
// Classify and tag new issues automatically
await issueTriageWorkflow.start({
title: event.issue.title,
body: event.issue.body,
issueNumber: event.issue.number,
repository: event.repository.full_name,
});
}

res.json({ received: true });
});

Webhook Best Practices

  • Always verify signatures using req.rawBody (not parsed JSON)
  • Acknowledge fast: Return 200 quickly, process asynchronously
  • Handle duplicates: Use webhook IDs for idempotency
  • Security: Store webhook secrets as encrypted environment variables

Workflow triggers and access patterns

Workflows can be triggered through multiple channels, each suited to different use cases:

Direct API Access

  • REST endpoints for direct workflow invocation
  • Workflow API for programmatic workflow management via the Workflow API
  • Perfect for integrating with other services or AI agents

Webhook Triggers

  • External events from third-party services (GitHub, Slack, Stripe) trigger workflows automatically
  • Built-in webhook signature verification with req.rawBody
  • Asynchronous processing with immediate acknowledgment
  • Perfect for event-driven automation

Trigger Comparison

Trigger TypeUse CaseBest For
REST APIDirect invocationUser-initiated actions, manual triggers
WebhooksExternal eventsGitHub issues, Slack messages, payment events
Scheduled JobsTime-basedPeriodic summaries, daily reports, cleanup tasks

For interactive prompting setups, see: ChatGPT backend API prompt.


When you might still want heavier tooling

You might still want Airflow, Prefect, or other orchestration platforms when you have:

  • Complex data pipelines with distributed processing and backfills
  • Strict SLAs that require fine-grained backpressure control
  • Python-native model ops where LangChain integrations are central

Codehooks can still host the execution layer for these systems (expose endpoints, persist results, schedule jobs), but it's not a replacement for large ETL platforms.


Wrap-up

If the hard part of your AI project is connecting steps reliably—not training models—then a light workflow runtime is usually enough. Codehooks gives you small functions, a state machine, storage, and a single deploy target.

These LLM workflows can be triggered and managed through APIs, CLI, or webhooks—giving you flexible control while maintaining clear operational boundaries.

  • Start with one file and one command
  • Add steps as your logic grows
  • Trigger via REST API, webhooks, or scheduled jobs

If you’re already calling OpenAI from Node, you’re only a few minutes away from turning your script into a resilient workflow.


LLM Workflow FAQ

Common questions about building LLM workflows, orchestration, and OpenAI API integration

What is an LLM workflow?
An LLM workflow is a multi-step process that chains together calls to Large Language Models (like OpenAI) with other operations like caching, storage, and API calls. It handles state management between steps, retries, scheduling, and persistence—the operational glue that makes AI applications reliable in production.
How is this different from LangChain or LlamaIndex?
LangChain and LlamaIndex are powerful prompt-chaining libraries focused on LLM interactions and RAG patterns. Codehooks workflows focus on the operational layer: state management between steps, retries, scheduling, persistence, and HTTP endpoints—without requiring heavyweight orchestration platforms like Airflow or Celery.
How do webhooks work with AI workflows?
Webhooks enable event-driven AI workflows. External services like GitHub, Slack, or Stripe can trigger workflows automatically when events occur. Codehooks provides built-in webhook signature verification using req.rawBody and supports asynchronous processing with immediate acknowledgment.
What does OpenAI API integration cost in these workflows?
Costs depend on model choice and token usage. Using gpt-4o-mini (recommended for most tasks) is significantly cheaper than GPT-4. Best practices include: setting max_tokens limits, caching responses for identical inputs, using lower temperatures for deterministic outputs, and monitoring usage in the OpenAI dashboard.
Can I use LLM providers other than OpenAI?
Yes. While the examples use OpenAI, you can adapt the workflow steps to call any LLM provider's API (Anthropic Claude, Google Gemini, etc.). Simply replace the fetch call in the workflow steps with the appropriate endpoint, headers, and request format for your chosen provider.
When should I use Codehooks instead of Airflow or Prefect?
Use Codehooks when your main challenge is connecting LLM steps reliably—not managing complex ETL pipelines. It's ideal for web developers who want state, retries, scheduling, and storage without spinning up orchestration infrastructure. Choose Airflow/Prefect for complex data pipelines with distributed processing, backfills, and strict SLAs.
What are the benefits of the Workflow API?
The Workflow API provides persistent state management, automatic retries, and step-by-step execution without requiring external orchestration tools. Each step is idempotent and can be re-run independently. You get built-in observability with workflow-status monitoring, and workflows can be triggered via REST API, webhooks, or scheduled jobs—all from a single JavaScript file.
Can I use Codehooks with AI coding agents like ChatGPT or Claude?
Yes! Codehooks is designed for AI agent integration. The simple JavaScript API (codehooks-js) makes it easy for AI agents to generate working code. You can use the ChatGPT backend API prompt to let AI agents build, deploy, and manage workflows through conversational interfaces. The platform handles deployment, persistence, and infrastructure, so agents can focus on business logic without dealing with DevOps complexity.

Useful links: Workflow API · ChatGPT backend API prompt