This code annotates the key components and explains their purpose.
This code is written for Next.js but looks very similar all frameworks or libraries.
It gives a good overview, but there are smaller more digestable exampels following up.
Stepsailor.tsx
'use client';
import { useEffect } from 'react';
import { CommandBarApi, defineSchema, fillableByAI, z } from '@stepsailor/sdk';
export default function StepsailorSdk() {
// Here you would load all the dependencies to contexts (e.g. your session)
//const session = getSession();
// Making sure it runs on mount and not to often
useEffect(() => {
setupSdk();
}, []);
return null;
}
// in here you define everything that command should know.
function setupSdk(/* in here you can define context that should be avaialbe */) {
const cmdBar = new CommandBarApi<{user: User}>();
// defineContext make sure every handler can access the user object,
// settings or more.
cmdBar.defineContext({user});
// An action is something that the AI can schedule based on the user input.
// Actions have an input schema which is partly visible to the AI agent
// You could imagine this like a tool call in agentic systems (but its not)
cmdBar.defineAction({
name: 'createAFolder', // <- camelCase name for AI Agent
description: 'Creates a folder with a given name', // <- description for AI Agent
displayName: 'Create a folder', // <- The display name for the user
// The input schema defines a typesafe runtime-typing.
inputSchema: defineSchema({
name: fillableByAI(z.string()), // <- AI agent will be able to fill this field
someParentFolder: z.string().optional() // <- not visible to the AI agent
}),
// The preview handler gives you the chance to validate the data
// the AI agent filled in and ask the user for other remaining input.
// This handler enables you to add human in the loop workflows in your cmdBar
previewHandler: async ({ name, someParentFolder }, context) => {
const { askForInput } = context.hil; // <- easiest way to get user input.
// code to ask for input (explained in further examples)
// code to display the selected input to make sure user is fine
// with AIs choices.
return {name, someParentFolder};
},
// The core of the action is the handler. This where you define
// what the action actually does
handler: async (input, context) => {
const { display } = context.hil;
// Informing the user about what the action is doing
display.log(`Creating folder with name ${input.name}`);
// You can run your server actions or api calls directly in here
// and reuse logic that you have already used in your UI.
const results = await serverActionForCreatingFolder({ input.name });
},
});
// This makes your defined actions available for the command bar
cmdBar.activate();
}
Input Schema
The schema makes the handler functions type-safe and enables you to define what the AI agent is allowed to fill in and what not. Giving you full control to reduce halluzinations.
You can define multiple types
The preview handlers run immediately after CmdBar AI came up with an action flow.
Its the place where you can make sure the input for the handler is valid and the user is fine with the selected data.
cmdBar.defineAction({
name: 'crawlData',
description: 'Crawls websites inorder to create reference data and stores it in a data store',
displayName: 'Crawl websites',
inputSchema: z.object({
websiteLinks: fillableByAI(z.array(z.string())),
dataStoreId: z.string(),
}),
previewHandler: async ({ websiteLinks, dataStoreId }, { hil }) => {
// dataStoreId will be undefined since AI can't know that.
const links = [];
// main human in'the loop interfaces...
const { askForInput, display } = hil;
// validating input...
if (websiteLinks && websiteLinks.length > 0) {
links.push(...websiteLinks);
} else {
const link = await askForInput.text({
label: 'Provide a link to crawl',
placeholder: 'https://www.doc.com/abc',
});
links.push(link);
}
// requesting dynamic / critical input
const dataStores = await getAvailableDataStores();
const _dataStoreId = await askForInput.enum({
label: 'Where should the data be stored?',
options: new Map(dataStores.map((ds) => [ds.id, ds.name])),
});
return { dataStoreId: _dataStoreId, websiteLinks: links };
},
handler: async (validatedInput, context) => {},
});
Handler (the action itself)
The handler runs for each action after the user hits "execute".
It has the same capabilities and api as the previewHandler but its supposed used for
executing the actual logic and only ask for input in edge cases.
cmdBar.defineAction({
name: 'crawlData',
description: 'Crawls websites inorder to create reference data and stores it in a data store',
displayName: 'Crawl websites',
inputSchema: z.object({
websiteLinks: fillableByAI(z.array(z.string())),
dataStoreId: z.string(),
}),
// [...] - preview handler
handler: async (validatedInput, context) => {
const { display } = context.hil;
display.log('The action is running');
await new Promise((resolve) => setTimeout(resolve, 1000));
display.log('And this log will stay and not replace the other');
await new Promise((resolve) => setTimeout(resolve, 1000));
const liveLog = display.syncedLog();
liveLog.log('...');
liveLog.log('currently its working on crawling...');
await new Promise((resolve) => setTimeout(resolve, 4000));
liveLog.log('crawling done');
await new Promise((resolve) => setTimeout(resolve, 6000));
liveLog.log('live logging updates the log in real time');
await new Promise((resolve) => setTimeout(resolve, 3000));
// closing the livelog connection
liveLog.close();
await new Promise((resolve) => setTimeout(resolve, 1000));
liveLog.hide();
display.log('The action is done and the live log is hidden');
},
});
The input schema is based on , because it is one of the most powerful static type inference toolings.