Skip to content

Advanced Patterns and Best Practices

Shared Parameters Pattern

When multiple tools share the same parameters (e.g., credentials, pagination, sorting), extract them as shared constants:

typescript
// _shared-parameters/credential.ts
export const credentialParameter: PropertyCredentialId = {
  name: "credential_id",
  type: "credential_id",
  credential_name: "notion",
  required: true,
}

// tools/create-page.ts
import { credentialParameter } from "../_shared-parameters/credential"

const createPageTool: ToolDefinition = {
  name: "create-page",
  parameters: [
    credentialParameter,
    // ... other parameters
  ],
  invoke: async ({
    args,
  }: {
    args: { parameters: { [key: string]: any }; credentials: Record<string, { api_key: string }> }
  }) => {
    const { parameters, credentials } = args
    const credential = credentials[parameters.credential_id]
    // Use credential to call Notion API
    return await notionApi.createPage(parameters, credential)
  },
}

Expression Support

Allow users to input dynamic expressions (reference upstream node data):

typescript
{
  name: "message",
  type: "string",
  ui: {
    component: "textarea",
    support_expression: true  // Enable expression mode
  }
}

invoke parameter example (expression values already parsed):

typescript
// User input expression: {{upstream_node.content}}
// System auto-parses expression, invoke receives actual value

const params = {
  message: "Hello, the page title is: Example Page",
}

invoke: async ({ args }) => {
  const { parameters } = args
  // parameters.message contains parsed content
  console.log(parameters.message)
  return { sent: true }
}

Constant Fields

Set field as read-only fixed value, commonly used in discriminated union:

typescript
{
  name: "type",
  type: "string",
  constant: "webhook",      // Value fixed as "webhook"
  display_name: { en_US: "Webhook" }
}

When constant is set:

  • Field displays as read-only
  • Value auto-populated on initialization
  • Cannot be modified by user

Nested Discriminated Union

Support multi-level nested discriminated unions:

typescript
{
  name: "action",
  type: "discriminated_union",
  discriminator: "type",
  any_of: [
    {
      name: "click",
      type: "object",
      properties: [
        { name: "type", type: "string", constant: "click" },
        // click has its own child discriminated union
        {
          name: "target",
          type: "discriminated_union",
          discriminator: "method",
          any_of: [
            {
              name: "css",
              type: "object",
              properties: [
                { name: "method", type: "string", constant: "css" },
                { name: "selector", type: "string" }
              ]
            },
            {
              name: "xpath",
              type: "object",
              properties: [
                { name: "method", type: "string", constant: "xpath" },
                { name: "expression", type: "string" }
              ]
            }
          ]
        }
      ]
    }
  ]
}

invoke parameter example (nested discrimination):

typescript
const params = {
  action: {
    type: "click",
    target: {
      method: "css",
      selector: ".submit-button",
    },
  },
}

// Or

const params2 = {
  action: {
    type: "click",
    target: {
      method: "xpath",
      expression: "//button[@id='submit']",
    },
  },
}

invoke: async ({ args }) => {
  const { parameters } = args
  const action = parameters.action
  if (action.type === "click") {
    const selector =
      action.target.method === "css" ? action.target.selector : action.target.expression
    await clickElement(selector, action.target.method)
  }
  return { success: true }
}

Array Elements as Discriminated Union

Each array item can be different object forms:

typescript
{
  name: "actions",
  type: "array",
  display_name: { en_US: "Actions" },
  items: {
    name: "action",
    type: "discriminated_union",
    discriminator: "type",
    any_of: [
      {
        name: "wait",
        type: "object",
        properties: [
          { name: "type", type: "string", constant: "wait", display_name: { en_US: "Wait" } },
          { name: "milliseconds", type: "integer", default: 1000 }
        ]
      },
      {
        name: "click",
        type: "object",
        properties: [
          { name: "type", type: "string", constant: "click", display_name: { en_US: "Click" } },
          { name: "selector", type: "string", display_name: { en_US: "Selector" } }
        ]
      },
      {
        name: "scroll",
        type: "object",
        properties: [
          { name: "type", type: "string", constant: "scroll", display_name: { en_US: "Scroll" } },
          { name: "direction", type: "string", enum: ["down", "up"], default: "down" }
        ]
      }
    ]
  }
}

invoke parameter example (mixed types in array):

typescript
const params = {
  actions: [
    {
      type: "wait",
      milliseconds: 2000,
    },
    {
      type: "click",
      selector: ".next-button",
    },
    {
      type: "scroll",
      direction: "down",
    },
    {
      type: "wait",
      milliseconds: 1000,
    },
    {
      type: "click",
      selector: ".load-more",
    },
  ],
}

invoke: async ({ args }) => {
  const { parameters } = args
  for (const action of parameters.actions) {
    switch (action.type) {
      case "wait":
        await sleep(action.milliseconds)
        break
      case "click":
        await clickElement(action.selector)
        break
      case "scroll":
        await scrollPage(action.direction)
        break
    }
  }
  return { success: true, actionsExecuted: parameters.actions.length }
}

Section UI Layout

Use section to display object name with underline at top, rendering child properties from top to bottom with indentation:

typescript
{
  name: "location",
  type: "object",
  display_name: { en_US: "Location" },
  ui: {
    component: "section"
  },
  properties: [
    { name: "country", type: "string", display_name: { en_US: "Country" } },
    { name: "languages", type: "array", items: { name: "l", type: "string" }, ui: { component: "tag-input" } }
  ]
}

invoke parameter example:

typescript
const params = {
  location: {
    country: "US",
    languages: ["English", "Spanish"],
  },
}

invoke: async ({ args }) => {
  const { parameters } = args
  const { country, languages } = parameters.location
  // Data structure is identical to normal object, but UI display is different
  console.log(`Selected country: ${country}`)
  console.log(`Languages available: ${languages.join(", ")}`)
  return { success: true }
}