A Next.js app demonstrating how to extract structured JSON from natural language questions using Fency's Structured Chat Completion task type.
- The AI answers questions about a set of actors and returns structured JSON matching a Zod-defined schema
- Each question is a stateless request — no conversation history is carried between turns
- A Zod schema is converted to a JSON Schema string and passed with each task so the model returns data in a predictable shape
- Node.js 18+
- A Fency account with a secret key and publishable key
-
Install dependencies:
npm install
-
Create a
.env.localfile in the project root and add your Fency keys:NEXT_PUBLIC_PUBLISHABLE_KEY=pk_... FENCY_SECRET_KEY=sk_...
-
Run the development server:
npm run dev
-
Open http://localhost:3000 and ask questions like "what is the age of johnny depp".
| Variable | Scope | Purpose |
|---|---|---|
NEXT_PUBLIC_PUBLISHABLE_KEY |
Client | Initializes the Fency SDK via loadFency |
FENCY_SECRET_KEY |
Server | Authenticates requests to the Fency REST API from API routes |
app/
page.tsx # SDK initialization and FencyProvider setup
app.tsx # Q&A UI component; defines the Zod response schema
api/
createSession.ts # Shared helper: POSTs to Fency sessions API
stream-session/route.ts # Creates a stream session (used by FencyProvider)
agent-task-session/route.ts # Creates a Structured Chat Completion session
lib/
fetchCreateStreamClientToken.ts # Fetches a stream client token from /api/stream-session
fetchCreateAgentTaskClientToken.ts # Fetches an agent task client token from /api/agent-task-session
actorInfo.ts # Static actor data text used as the system prompt context
hooks/
useActorQandA.ts # Custom hook wrapping useAgentTasks for structured Q&A
The Fency SDK is initialized once at the module level using the publishable key. The FencyProvider wraps the app and receives a fetchCreateStreamClientToken callback the SDK uses internally to authenticate real-time streams.
const fency = loadFency({
publishableKey: process.env.NEXT_PUBLIC_PUBLISHABLE_KEY!,
})
export default function Home() {
return (
<FencyProvider
fency={fency}
fetchCreateStreamClientToken={fetchCreateStreamClientToken}
>
<App />
</FencyProvider>
)
}A shared utility used by both API routes. It calls POST https://api.fency.ai/v1/sessions with the FENCY_SECRET_KEY and returns the response (which includes a short-lived clientToken). The secret key never leaves the server.
Creates a stream session used by FencyProvider for real-time task updates:
export async function POST() {
return await createSession({ createStream: {} })
}Creates a Structured Chat Completion session:
export async function POST() {
return await createSession({
createAgentTask: { taskType: 'STRUCTURED_CHAT_COMPLETION' },
})
}Both fetchers call their respective API routes and return { clientToken }.
fetchCreateStreamClientToken— callsPOST /api/stream-session; passed toFencyProviderfetchCreateAgentTaskClientToken— callsPOST /api/agent-task-session; passed touseActorQandA
useAgentTasks from @fencyai/react is the core hook. It returns:
agentTasks— a reactive array of task objects, each containingparams,error, and live result statecreateAgentTask— an async function that starts a new task and returns its result
The hook requires a fetchCreateAgentTaskClientToken callback it uses to obtain an authenticated token before each task.
useActorQandA wraps useAgentTasks for stateless structured Q&A. Unlike the chat examples, there is no conversation history — every question is an independent request. On each question it:
- Builds a fresh
[system, user]message pair: the system message contains the static actor data fromACTOR_INFO_TEXT; the user message is the question - Calls
createAgentTaskwithtype: 'StructuredChatCompletion', the messages, the model, and ajsonSchemastring
The jsonSchema is generated in app.tsx by converting a Zod schema with z.toJSONSchema:
// app/app.tsx
const actorResponseSchema = z.object({
actorName: z.string().describe('Name of the actor'),
age: z.string().describe('Age of the actor'),
})
// Passed to the hook as a JSON string
jsonSchema: JSON.stringify(z.toJSONSchema(actorResponseSchema))The hook call:
await createAgentTask({
type: 'StructuredChatCompletion',
messages, // [{ role: 'system', content: actorInfoText }, { role: 'user', content: question }]
model, // 'anthropic/claude-sonnet-4.5'
jsonSchema, // JSON Schema string derived from the Zod schema
})The model returns a JSON object matching the schema (e.g. { "actorName": "Johnny Depp", "age": "60" }), which is displayed via the AgentTaskProgress component.