Skip to content

Commit 67275ba

Browse files
committed
feat: expose partial tools and expand workflow email usage
1 parent db2521d commit 67275ba

File tree

2 files changed

+203
-27
lines changed

2 files changed

+203
-27
lines changed

src/lib/tools/partials.ts

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,90 @@ const listPartials = KnockTool({
4747
},
4848
});
4949

50+
const getPartial = KnockTool({
51+
method: "get_partial",
52+
name: "Get partial",
53+
description: `
54+
Get a partial by its key. Use this tool when you need to know if a specific partial exists by key.
55+
`,
56+
parameters: z.object({
57+
environment: z
58+
.string()
59+
.optional()
60+
.describe(
61+
"(string): The environment to get the partial for. Defaults to `development`."
62+
),
63+
key: z.string().describe("(string): The key of the partial to get."),
64+
}),
65+
execute: (knockClient, config) => async (params) => {
66+
const partial = await knockClient.partials.retrieve(params.key, {
67+
environment: params.environment ?? config.environment ?? "development",
68+
});
69+
70+
return serializePartial(partial);
71+
},
72+
});
73+
74+
const upsertPartial = KnockTool({
75+
method: "upsert_partial",
76+
name: "Upsert partial",
77+
description: `
78+
Create or update a partial. A partial is a reusable piece of content that can be used in a template. Use this tool when you need to create a new partial or update an existing one.
79+
80+
When working with a partial you must chose the type of the partial. The type determines the format of the content. If you're working with an email template, you should use the "html" or "markdown" type.
81+
82+
If you need to work with dynamic content in your partial you can use liquid syntax. Liquid is a templating language that is supported in Knock. You can supply a variable like {{ some_variable }} in the content and it will be replaced with the actual value when the partial is used in a template.
83+
84+
<example>
85+
{
86+
"name": "Greeting",
87+
"key": "greeting",
88+
"type": "html",
89+
"content": "<div>Hello, {{ recipient_name }}</div>"
90+
}
91+
</example>
92+
93+
Changes to a partial MUST be committed before they can be used in a template.
94+
`,
95+
parameters: z.object({
96+
environment: z
97+
.string()
98+
.optional()
99+
.describe(
100+
"(string): The environment to upsert the partial for. Defaults to `development`."
101+
),
102+
key: z.string().describe("(string): The key of the partial to upsert."),
103+
name: z.string().describe("(string): The name of the partial."),
104+
description: z
105+
.string()
106+
.optional()
107+
.describe("(string): The description of the partial."),
108+
content: z.string().describe("(string): The content of the partial."),
109+
type: z
110+
.enum(["html", "text", "json", "markdown"])
111+
.describe("(string): The type of the partial."),
112+
}),
113+
execute: (knockClient, config) => async (params) => {
114+
const partial = await knockClient.partials.upsert(params.key, {
115+
environment: params.environment ?? config.environment ?? "development",
116+
partial: {
117+
name: params.name,
118+
description: params.description,
119+
content: params.content,
120+
type: params.type,
121+
},
122+
});
123+
return serializePartial(partial.partial);
124+
},
125+
});
126+
50127
export const partials = {
128+
getPartial,
51129
listPartials,
130+
upsertPartial,
52131
};
53132

54133
export const permissions = {
55-
read: ["listPartials"],
134+
read: ["getPartial", "listPartials"],
135+
manage: ["upsertPartial"],
56136
};

src/lib/tools/workflows.ts

Lines changed: 122 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -136,44 +136,145 @@ const triggerWorkflow = KnockTool({
136136
});
137137

138138
const createEmailWorkflow = KnockTool({
139-
method: "create_email_workflow",
140-
name: "Create email workflow",
139+
method: "create_rich_email_workflow",
140+
name: "Create rich email workflow",
141141
description: `
142-
Creates a simple email workflow with a single step that sends an email to the recipient. Use this tool when you need to need to create an email notification, and you don't need to specify any additional steps. You can only create workflows in the development environment.
142+
Creates a Knock workflow with a single step for sending an email. Use this tool when you're asked to create an email notification and you need to specify the content of the email.
143143
144-
The content of the email you supply should ONLY ever be in markdown format for simplicity. You can supply dynamic variables to the subject and body of the email using the liquid template language.
144+
## Blocks
145145
146-
When writing markdown, be sure to use headings (##) to separate sections of the email. Use an informal writing style, and avoid using complex language.
146+
The content of the email is supplied as an array of "blocks". The simplest block is a "markdown" block, which supports content in a markdown format. That should always be your default block type.
147147
148-
The following variables are available to use in the email subject and body:
148+
The following block types are supported:
149149
150+
- \`markdown\`: A block that supports markdown content.
151+
- \`html\`: A block that supports markdown content.
152+
- \`image\`: A block that supports an image.
153+
- \`button_set\`: A block that adds one or more buttons.
154+
- \`divider\`: A block that supports a divider.
155+
- \`partial\`: A block that supports rendering a shared content partial.
156+
157+
<example>
158+
{
159+
"blocks": [
160+
{
161+
"type": "markdown",
162+
"content": "# Greetings from Knock!\nHello, {{ recipient.name }}."
163+
},
164+
{
165+
"type": "divider"
166+
},
167+
{
168+
"type": "button_set",
169+
"buttons": [
170+
{
171+
"label": "Approve",
172+
"action": "{{ data.primary_action_url }}",
173+
"variant": "solid"
174+
}
175+
]
176+
}
177+
]
178+
}
179+
</example>
180+
181+
### Markdown
182+
183+
When using the \`markdown\` block, you must supply a \`content\` key. The \`content\` key supports markdown.
184+
185+
<example>
186+
{
187+
"type": "markdown",
188+
"content": "Hello, world!"
189+
}
190+
</example>
191+
192+
### HTML
193+
194+
The \`html\` block supports raw HTML content. This should be used sparingly, and only when you need to include custom HTML content that markdown doesn't support. When using the \`html\` block, you must supply a \`content\` key. HTML content can include liquid personalization.
195+
196+
### Button sets
197+
198+
Button sets are a special type of block that allows you to add one or more buttons to the email. They're useful for directing users to take specific actions. Button sets support one or more buttons. You must always include at least one button in a button set.
199+
200+
Buttons are specified in a button set under the \`buttons\` key. Each button requires a \`label\`, \`action\`, and \`variant\`. The ONLY valid variants are \`solid\` and \`outline\`. The label and action can allowed be dynamic variables using liquid.
201+
202+
<example>
203+
{
204+
"type": "button_set",
205+
"buttons": [
206+
{
207+
"label": "Approve",
208+
"action": "https://example.com",
209+
"variant": "solid"
210+
}
211+
]
212+
}
213+
</example>
214+
215+
### Image
216+
217+
Images are a special type of block that allows you to add an image to the email. When using the \`image\` block, you must supply a \`url\` key. The \`url\` key supports a URL to an image.
218+
219+
<example>
220+
{
221+
"type": "image",
222+
"url": "https://example.com/image.png"
223+
}
224+
</example>
225+
226+
## Personalization
227+
228+
If you need to include personalization, you can use liquid to include dynamic content in the email and the subject line.
229+
The following variables are always available to use in liquid:
230+
231+
- \`recipient.id\`: The ID of the recipient.
150232
- \`recipient.name\`: The name of the recipient.
151233
- \`recipient.email\`: The email of the recipient.
152234
- \`recipient.phone_number\`: The phone number of the recipient.
153-
- \`tenant.id\`: The id of the tenant.
154-
- \`tenant.name\`: The name of the tenant.
155235
156-
You can supply any other dynamic variables by referencing them under the \`data\` key in the \`data\` parameter when triggering the workflow. You add those like \`{{ data.variable_name }}\`.
236+
You can supply **any** other dynamic variables you think are needed by referencing them under the \`data\` key. You add those like \`{{ data.variable_name }}\`.
237+
238+
<example>
239+
# Hello, {{ recipient.name }}
240+
241+
This is a dynamic message:
242+
243+
> {{ data.message }}
244+
</example>
245+
246+
## Liquid helpers
247+
248+
You have access to a full suite of liquid helpers to help you perform common templating tasks. The full list of helper is available here: https://docs.knock.app/designing-workflows/template-editor/reference-liquid-helpers.
249+
250+
<example>
251+
Hello, {{ recipient.name | split: " " | first | default: "there" }}
252+
</example>
253+
254+
## Partials
255+
256+
If you need to reuse content across multiple emails, you can create or reference an existing partial and reference it in the email. You should only use partials if you're instructed to do so.
157257
158-
You can also supply a list of categories to the workflow. These are used to categorize workflows for notification preferences. Categories should be supplied as lowercase strings in kebab case.
258+
When you do need to use a partial in an email, you can use the \`partial\` block and then set the \`key\` to the key of the partial you want to use. If the partial requires any variables, you pass those in the \`attrs\` key.
159259
160-
Once you've created the workflow, you should ask if you should commit the changes to the environment.
260+
## Writing style
261+
262+
Unless asked otherwise, you should write content for the email in a concise and formal writing style. Do NOT use complex language or try to over explain. Keep the subject line to 8 words or less.
161263
`,
162264
parameters: z.object({
163-
environment: z
265+
name: z.string().describe("(string): The name of the workflow."),
266+
description: z
164267
.string()
165268
.optional()
166-
.describe(
167-
"(string): The environment to create the workflow in. Defaults to `development`."
168-
),
169-
workflowKey: z.string().describe("(string): The key of the workflow."),
170-
name: z.string().describe("(string): The name of the workflow."),
269+
.describe("(string): The description of the workflow."),
171270
categories: z
172271
.array(z.string())
173272
.optional()
174273
.describe("(array): The categories to add to the workflow."),
274+
blocks: z
275+
.array(z.any())
276+
.describe("(array): The blocks to add to the workflow."),
175277
subject: z.string().describe("(string): The subject of the email."),
176-
body: z.string().describe("(string): The body of the email."),
177278
}),
178279
execute: (knockClient, config) => async (params) => {
179280
const emailChannelsPage = await knockClient.channels.list();
@@ -186,9 +287,10 @@ const createEmailWorkflow = KnockTool({
186287
}
187288

188289
const workflowParams: WorkflowUpsertParams = {
189-
environment: params.environment ?? config.environment ?? "development",
290+
environment: config.environment ?? "development",
190291
workflow: {
191292
name: params.name,
293+
description: params.description,
192294
categories: params.categories ?? [],
193295
steps: [
194296
{
@@ -199,13 +301,7 @@ const createEmailWorkflow = KnockTool({
199301
layout_key: "default",
200302
},
201303
subject: params.subject,
202-
visual_blocks: [
203-
// @ts-ignore
204-
{
205-
type: "markdown",
206-
content: params.body,
207-
},
208-
],
304+
visual_blocks: params.blocks,
209305
},
210306
name: "Email",
211307
ref: "email_1",

0 commit comments

Comments
 (0)