Welcome back! In Task Lifecycle Workflow, we programmed the logic of our tool. We decided how to handle updates, deletions, and status changes.
However, before our logic ever runs, we need to ensure the data coming in is correct. If the AI sends us a number when we expect a text string, or a status that doesn't exist, our code might crash.
To prevent this, we use Schema Validation. In this chapter, we will learn a special technique called Lazy Schema Validation to keep our tool fast and flexible.
In programming, we often face the "Chicken and Egg" problem (also known as Circular Dependencies).
Imagine you are defining a Team:
Team is made of Members.Member belongs to a Team.If you try to define both strictly at the exact same moment the application starts, the computer gets confused: "I can't define Team because I don't know what a Member is yet, but I can't define Member because I don't know what a Team is!"
Imagine a high-security building with a guard at the door.
At 8:00 AM, the guard memorizes the face and ID number of every single person who might visit the building that day. This takes hours. The building cannot open until he is done.
The guard sits back and relaxes. He doesn't memorize anything. When a visitor actually arrives at 10:30 AM, only then does he open the manual to check that specific person's credentials.
Lazy Schema Validation is the second approach. We tell our tool: "Don't compile the rules yet. Wait until someone actually asks to use this tool."
lazySchema
To implement this, we wrap our definitions in a function using a helper called lazySchema.
Instead of a static object, we write a function that returns the object.
import { lazySchema } from '../../utils/lazySchema.js'
import { z } from 'zod/v4'
// We define the schema inside a function () => { ... }
const inputSchema = lazySchema(() => {
// Rules are compiled only when this function is called
return z.strictObject({
taskId: z.string().describe('The ID of the task'),
// ... other fields
})
})
Explanation:
The () => { ... } arrow function acts as a pause button. The code inside the curly braces doesn't run when the file loads. It waits until the tool is needed.
Now, let's look at the actual inputs for our TaskUpdateTool. We use a library called Zod (z) to define the "types" (string, number, boolean).
In the previous chapter, we learned that deleted is a special action, distinct from standard statuses like in_progress. We need to reflect that in our validation.
const inputSchema = lazySchema(() => {
// Combine standard statuses with the "deleted" action
const TaskUpdateStatusSchema = TaskStatusSchema().or(z.literal('deleted'))
return z.strictObject({
taskId: z.string().describe('The ID of the task to update'),
status: TaskUpdateStatusSchema.optional(),
subject: z.string().optional(),
})
})
Explanation:
TaskStatusSchema(): Fetches the standard list of statuses (todo, done, etc.)..or(z.literal('deleted')): Allows the AI to send the word "deleted" as a valid status, even if it's technically a destructive action.z.strictObject: This creates the "form." If the AI sends extra fields we didn't ask for, Zod will reject them.Just as we validate inputs, we must describe what we send back to the AI. This helps the AI understand if it succeeded or failed.
We use lazySchema here as well, mostly for consistency and performance.
const outputSchema = lazySchema(() =>
z.object({
success: z.boolean(),
taskId: z.string(),
updatedFields: z.array(z.string()),
error: z.string().optional(),
})
)
Explanation:
This schema guarantees that our call function (from Chapter 2) always returns a structured object containing success (true/false) and the updatedFields.
Finally, we need to plug these lazy schemas into our buildTool definition from Chapter 1.
Because we used lazySchema, we need to use TypeScript "getters" (functions that look like properties) to unwrap them.
export const TaskUpdateTool = buildTool({
name: 'TaskUpdate',
// The 'get' keyword calls our lazy function
get inputSchema() {
return inputSchema()
},
get outputSchema() {
return outputSchema()
},
// ... rest of the tool
})
Explanation:
When the system asks TaskUpdateTool.inputSchema, the get function runs inputSchema(). This triggers the "Security Guard" to finally open the manual and compile the rules.
What happens internally when the AI decides to use the tool?
Our TaskUpdateTool interacts with TaskStatusSchema.
TaskStatusSchema might depend on configuration files loaded from a database.
For those using TypeScript, lazySchema provides a helper to infer types automatically. You don't need to write interface definitions manually.
// Automatically determines the type based on the Zod rules
type InputSchema = ReturnType<typeof inputSchema>
type OutputSchema = ReturnType<typeof outputSchema>
// We can now use this type in our code
export type Output = z.infer<OutputSchema>
Explanation:
ReturnType looks at our lazy function and figures out what shape the data will take. If we change the Zod definition later (e.g., adding a priority field), our TypeScript types update automatically!
In this chapter, we learned:
lazySchema wraps our rules in a function, acting like a "pause button."TaskStatus) with special cases (like deleted).get inputSchema) trigger the compilation only when needed.Now that we have the inputs validated and the logic defined, we need to look at the relationships between tasks. What happens if Task A cannot be started until Task B is finished?
Next Chapter: Task Dependency Management
Generated by Code IQ