Guide: Create a Template
Step by step guide creating a new template
In this guide we'll go step by step creating a new template. We will create the Info Agent template. This template will allow creators to add some invisible context around their post so that everytime someone comments on the post Sage will automatically reply with answers about whatever they may ask.
Step 1: New File
Copy an existing template file such as artistPresent.ts
in the templates folder in client-bonsai and name it infoAgent.ts
. Rename the function infoAgent and update the default export at the bottom of the file to reflect the new function name.
Step 2: Update the clientMetadata
Update the clientMetadata
object:
clientMetadata: {
protocolFeeRecipient: "0x...", // set your address here to receive rev share
category: TemplateCategory.CAMPFIRE,
name: TemplateName.INFO_AGENT,
displayName: "Info Agent",
description:
"The info agent is a template that allows an AI to respond to comments on a post.",
image: "https://link.storjshare.io/raw/jvbkb2ge7rha75xa53sdbclgerlq/bonsai/info_agent.webp",
options: {
allowPreview: false,
allowPreviousToken: true,
imageRequirement: ImageRequirement.OPTIONAL,
requireContent: true,
},
defaultModel: getModelSettings(ModelProviderName.OPENAI, ModelClass.MEDIUM)?.name,
templateData: {
form: z.object({
info: z
.string()
.describe(
"Provide information about the topic you want the agent to respond to"
),
urls: z
.string()
.describe(
"List of URLs containing additional information for the agent to reference separated by commas"
),
}),
},
},
Go to utils/types.ts
to add the new TemplateCategory
and TemplateName
. Then we set both of those properties on the object here along with the new display name, description, and we get a link to a new image.
For the options object we set allowPreview
to false since there won't be any media updates for this template type and no generative post content. Image is optional and content is required since creator's will need to create the original post content. This will prompt the dynamic form to allow the creator to write the post text.
Lastly update the templateData
object with the form items that creators will need to provide to create the post. In this case we will allow them to provide any info that may be relevant and urls that the AI can scrape such as docs for responding to comments.
Step 3: Update the Prompt Template
Next we need to update the template that is used to prompt the AI. We are going to batch comments so that it can respond to multiple at a time with one API request.
export const replyTemplate = `
# Instructions
You are an agent commentator that is responding to the content of a social post. The social post content is:
{{postContent}}
Some additional information about the post is:
{{info}}
Use these websites to get more information about the post:
{{urls}}
Your job is to respond to the following comments. Reply with a JSON formatted object that is a reply to the index of the comment you are replying to. Format the reply as a JSON array with the following properties where each object in the array represents a reply to a comment:
\`\`\`json
[
{
reply_to: number,
text: string
},
...
]
\`\`\`
# Comments
{{comments}}
`;
Here we instruct the AI to take in the comments and the info and urls provided by the creator and return a JSON object of responses to each one. We also need to create some new types.
type TemplateData = {
info: string;
urls: string;
};
type Reply = {
reply_to: number;
text: string;
};
Step 4: Update the Handler Function
If you copied a different template file then the handler function will start with these lines:
elizaLogger.log("Running template:", TemplateName.INFO_AGENT);
if (!media?.templateData) {
elizaLogger.error("Missing template data");
return;
}
const templateData = media.templateData as TemplateData;
let totalUsage: TemplateUsage = {
promptTokens: 0,
completionTokens: 0,
totalTokens: 0,
imagesCreated: 0,
};
And then continue with a try catch statement. Delete everything inside the try block. In order to process comments and respond to them we need to:
fetch recent comments
fetch the original post content
generate responses to the comments
respond to the comments
return values
Step 4.1: Fetch recent comments
Use this code to fetch recent comments made since the last update.
let comments: Post[]; // latest comments to evaluate for the next decision
// fetch comments and respond to/tip them
const allComments = await fetchAllCommentsFor(
media?.postId as string
);
comments = getLatestComments(media as SmartMedia, allComments);
Step 4.2: Fetch the original post content
Use this code to fetch the content of the original post.
// fetch the post content
const result = await fetchPost(client, {
post: postId(media?.postId as string),
});
let postContent = "";
if (result.isErr()) {
elizaLogger.error("Error fetching post", result.error);
} else {
postContent = result.value?.metadata?.content;
}
Step 4.3: Generate responses to the comments
Compose the context of the AI request and then generate the object and track the token usage. Make sure to include the web search tool so that the model can use your urls.
// generate reponses to the comments
const context = composeContext({
// @ts-expect-error we don't need the full State object here to produce the context
state: {
postContent,
info: templateData?.info,
urls: templateData?.urls,
comments: comments
.map((c) => (c.metadata as TextOnlyMetadata).content)
.join("\n"),
},
template: replyTemplate,
});
const { response: replies, usage } =
(await generateObjectDeprecated({
runtime,
context,
modelClass: ModelClass.MEDIUM,
modelProvider: ModelProviderName.OPENAI,
returnUsage: true,
tools: {
web_search_preview: openai.tools.webSearchPreview(),
},
})) as unknown as {
response: Reply[];
usage: LanguageModelUsage;
};
totalUsage.promptTokens += usage.promptTokens;
totalUsage.completionTokens += usage.completionTokens;
totalUsage.totalTokens += usage.totalTokens;
Step 4.4: Respond to the comments
Loop through the comments and send a reply to each one.
// respond to the comments
const signer = privateKeyToAccount(
process.env.EVM_PRIVATE_KEY as `0x${string}`
);
const sessionClient = await authenticate(signer, "bons_ai");
for (let i = 0; i < comments.length; i++) {
const result = await createPost(
sessionClient!,
signer,
{ text: replies[i].text },
comments[i].id
);
if (result) {
elizaLogger.log("Successfully replied to comment", result);
} else {
elizaLogger.error("Failed to reply to comment", result);
}
}
Step 4.5: Return values
Lastly return from the handler. Since we aren't updating the metadata or uri those values will be undefined so that the server knows not to push anything new to Lens or trigger a metadata refresh.
// metdata and uri dont change
return { metadata: undefined, updatedUri: undefined, totalUsage };
Step 5: Update the server to broadcast the template
Update the initialize function in index.ts
to include the new template.
import infoAgentTemplate from "./templates/infoAgent";
...
/**
* Initializes MongoDB connection and registers available templates.
*/
private async initialize() {
this.mongo = await getClient();
// init templates
for (const template of [infoAgentTemplate, ...]) { // include any other template here
this.templates.set(template.clientMetadata.name, template);
};
}
Testing
To test the template we'll start up the server and open a tunnel with ngrok to a public url. Then we'll import the url on https://testnet.onbons.ai/studio to dynamically load the post creation form.
Import template and create post
Setup with ngrok
Install ngrok if you don't have it already
npm install -g ngrok
# or
brew install ngrok
# or download directly from https://ngrok.com/download
Authenticate
ngrok config add-authtoken YOUR_AUTH_TOKEN
Route with ngrok
ngrok http 3001
Get the ngrok url and add it to the
.env
of your Eliza server asDOMAIN
DOMAIN=https://someurl.ngrok.app # replace with your url
Start the server
pnpm build && pnpm start
Paste url into the import section of the studio

The new template should appear at the bottom of the list. Use a cover image with an aspect ratio of 3 to 2 for best results. The imported url will be saved in your browser's local storage until you either clear it or import a new url. Only one imported url will be saved at a time.

Click on "Create" and you should be directed to a new page with a form like the one you specified in the zod object of clientMetadata.templateData
.

You should be able to create a new post with the new template now. Then we'll be able to activate the post lifecycle on the Eliza server to create updates.
Trigger Updates
Now you can trigger an update to your post. First leave a comment or two on the post and then send a POST command to the update endpoint on the server.
curl -X POST \
-H "x-api-key: {YOUR API KEY}" \ # this is set in your .env ISSUED_API_KEYS
# you can get the post slug from the url e.g. https://testnet.bonsai.meme/post/2qrc5ghp1cwxy9sy0qy
http://localhost:3001/post/{YOUR POST SLUG HERE}/update
You should see logs of the post content and comments being fetched and new content being generated and Lens content uris being created as responses are sent out to each of the comments.
That's it!
Last updated