CRUD workflows
Common content-management workflows — creating, listing, updating, organizing, and bulk-importing questions and categories.
Updated 2026-05-20
The day-to-day patterns for managing FAQ content. Every example uses @faqapp/core; the equivalent cURL is in the API reference.
Setup
import { createFAQClient } from "@faqapp/core";
const faq = createFAQClient({
apiKey: process.env.FAQAPP_API_KEY!,
organizationSlug: "acme"
});
The client wraps every endpoint with typed methods, automatic retries on transient errors, and the typed error classes from Errors.
Create a question
const question = await faq.questions.create({
title: "How do I cancel my subscription?",
answer: "Open Settings → Billing → Cancel subscription. You keep access through the end of the billing period.",
categorySlug: "billing",
status: "published"
});
console.log(question.id); // "q_8X3F"
console.log(question.slug); // "how-do-i-cancel-my-subscription"
categorySlug is a convenience over categoryId — pass either. Slug auto-derives from title unless you override with slug:.
For idempotent writes (deduping at retry time), pass idempotencyKey:
await faq.questions.create(
{ title: "…", answer: "…", categorySlug: "billing" },
{ idempotencyKey: "import-row-42" }
);
List with cursor pagination
const { data, meta } = await faq.questions.list({
status: "published",
limit: 50
});
console.log(data.length); // up to 50
console.log(meta.pagination.hasMore); // true if there's a next page
Walk every page:
let cursor: string | null = null;
const all: Question[] = [];
do {
const page = await faq.questions.list({ limit: 100, cursor });
all.push(...page.data);
cursor = page.meta.pagination.cursor;
} while (cursor);
console.log(`fetched ${all.length} questions`);
Don’t parallelize page fetches — cursors are server-side state; fan-out is order-undefined.
Update a question
const updated = await faq.questions.update("q_8X3F", {
answer: "Updated answer. Each update creates a revision (max 50 per question)."
});
Partial — only the fields you pass are touched. Every update writes a revision; the most recent 50 are kept per question. See Revisions below.
Delete a question
await faq.questions.delete("q_8X3F");
// 204 No Content. Soft delete; purged after 30 days.
Returns nothing on success. The NotFoundError class is thrown if the id doesn’t exist.
Create + organize categories
const billing = await faq.categories.create({
name: "Billing",
description: "Subscriptions, invoices, refunds.",
order: 1
});
await faq.categories.create({ name: "API", order: 2 });
await faq.categories.create({ name: "Security", order: 3 });
order controls sort. Lower numbers come first. Use multiples of 10 (10, 20, 30) so you can insert between them later without renumbering everything.
To move a question between categories:
await faq.questions.update("q_8X3F", { categorySlug: "billing" });
Bulk import
For initial migration from CSV or another tool, use the create-many helper:
import { parse } from "csv-parse/sync";
import { readFileSync } from "node:fs";
const rows = parse(readFileSync("faq-export.csv"), { columns: true });
const results = await Promise.allSettled(
rows.map((row, i) =>
faq.questions.create(
{
title: row.question,
answer: row.answer,
categorySlug: row.category
},
{ idempotencyKey: `import-${row.id}` }
)
)
);
const ok = results.filter((r) => r.status === "fulfilled").length;
const failed = results.filter((r) => r.status === "rejected");
console.log(`${ok} created, ${failed.length} failed`);
for (const f of failed) console.error((f as PromiseRejectedResult).reason);
The idempotencyKey makes the whole script safely re-runnable — if it crashes halfway, re-run and only the missing rows actually create.
Revisions and history
Every update creates a revision. Read the history:
const revisions = await faq.questions.revisions("q_8X3F");
// Array<{ revision: number; title: string; answer: string; createdAt: Date }>
Restore an old revision by passing its contents to update:
const r = revisions[5];
await faq.questions.update("q_8X3F", { title: r.title, answer: r.answer });
Storage rolls — revision 51 evicts revision 1.
Translations
Once you’ve configured target languages (see Translations), translate a question with AI then edit before publish:
await faq.translations.questionAi("how-do-i-cancel", {
languages: ["es", "de", "fr"]
});
// All three are now saved as status: 'draft' for your review.
await faq.translations.questionUpdate("how-do-i-cancel", "es", {
answer: "Cleaner Spanish answer."
});
await faq.translations.questionUpdate("how-do-i-cancel", "es", {
status: "published"
});
Source language stays authoritative — if a reader’s preferred language isn’t published yet, they see the source.