Skip to content

Advanced Features

FlowKit includes powerful features for building sophisticated conversational flows.

Inline Actions

Execute code directly within a step without needing to register a tool.

typescript
flow("order-flow", agent)
  .ask("get_email", "What's your email?", email(), "user_email")
  .do(async (ctx) => {
    // Inline action - runs after extraction
    console.log(`New lead: ${ctx.slots.user_email}`);
    await analytics.track("email_captured", { email: ctx.slots.user_email });
  })
  .then("next_step")

Use Cases

  • Quick logging/analytics
  • Simple data validation
  • Triggering side effects
  • Modifying slots programmatically
typescript
.do(async (ctx) => {
  // Modify slots
  ctx.slots.normalized_name = ctx.slots.name.trim().toLowerCase();
  
  // Fetch external data
  const user = await db.findByEmail(ctx.slots.email);
  if (user) {
    ctx.slots.existing_customer = true;
    ctx.slots.customer_tier = user.tier;
  }
})

Conditional Steps

Skip or include steps based on conditions.

.skipIf() - Skip a step when condition is true

typescript
flow("checkout", agent)
  .ask("get_email", "What's your email?", email(), "email")
  .then("ask_address")
  
  // Skip if we already have the address from a previous interaction
  .ask("ask_address", "What's your shipping address?", text(), "address")
  .skipIf((ctx) => !!ctx.slots.saved_address)
  .then("confirm_order")

.onlyIf() - Include step only when condition is true

typescript
flow("survey", agent)
  .ask("age", "How old are you?", number(), "age")
  .then("ask_job")
  
  // Only ask about job if user is 18+
  .ask("ask_job", "What's your occupation?", text(), "job")
  .onlyIf((ctx) => ctx.slots.age >= 18)
  .then("thanks")

Complex Conditions

typescript
// Multiple conditions
.skipIf((ctx) => 
  ctx.slots.is_vip || 
  ctx.slots.skip_verification === true
)

// Based on previous answers
.onlyIf((ctx) => 
  ctx.slots.issue_type === "technical" &&
  ctx.slots.severity === "high"
)

Slot Transformations

Transform extracted values before storing them.

Built-in Transforms

typescript
flow("registration", agent)
  .ask("get_name", "What's your name?", name(), "user_name")
  .transform("capitalize")  // "john doe" -> "John Doe"
  .then("get_email")
  
  .ask("get_email", "What's your email?", email(), "user_email")
  .transform("lowercase")   // "John@Email.COM" -> "john@email.com"
  .then("get_code")
  
  .ask("get_code", "Enter your code:", text(), "code")
  .transform("uppercase")   // "abc123" -> "ABC123"
  .then("get_phone")
  
  .ask("get_phone", "Phone number?", text(), "phone")
  .transform("digits")      // "(123) 456-7890" -> "1234567890"
  .then("done")

Available Transforms

TransformDescriptionExample
lowercaseConvert to lowercase"ABC" → "abc"
uppercaseConvert to uppercase"abc" → "ABC"
capitalizeCapitalize words"john doe" → "John Doe"
trimRemove whitespace" hello " → "hello"
digitsExtract only digits"(123) 456" → "123456"

Custom Transforms

typescript
.ask("get_price", "What's your budget?", text(), "budget")
.transform((value) => {
  // Parse price and normalize
  const num = parseFloat(value.replace(/[^0-9.]/g, ""));
  return Math.round(num * 100) / 100; // Round to 2 decimals
})
.then("next")

// Async transforms
.ask("get_sku", "Enter product SKU:", text(), "sku")
.transform(async (value, ctx) => {
  const product = await db.findBySku(value);
  ctx.slots.product_name = product?.name;
  return value.toUpperCase();
})

Yes/No Shortcuts

Simplify branching for yes/no questions.

Basic Usage

typescript
flow("confirmation", agent)
  .ask("confirm", "Do you want to proceed?", yesNo(), "confirmed")
  .ifYes("process_order")
  .ifNo("cancel_order")
  
  .say("process_order", "Processing your order...")
  .done()
  
  .say("cancel_order", "Order cancelled.")
  .done()

Equivalent to

typescript
.ask("confirm", "Do you want to proceed?", yesNo(), "confirmed")
.when({ yes: "process_order", no: "cancel_order" })

Retry Messages

Customize the retry message when extraction fails.

typescript
flow("email-collector", agent)
  .ask("get_email", "Please enter your email address:", email(), "email")
  .retry("Hmm, that doesn't look like a valid email. Please try again:")
  .then("done")

Default Behavior

Without .retry(), the engine uses a generic retry message based on the extract type.


New Extractors

url() - Extract URLs

typescript
import { url } from "@andresaya/flowkit";

flow("link-collector", agent)
  .ask("get_website", "What's your website?", url(), "website")
  .then("done")

Extracts valid URLs from user messages:

  • https://example.com
  • http://localhost:3000
  • www.example.com (adds https://)

time() - Extract Time Values

typescript
import { time } from "@andresaya/flowkit";

flow("scheduler", agent)
  .ask("get_time", "What time works for you?", time(), "appointment_time")
  .then("confirm")

Extracts time in various formats:

  • "3pm", "3:30pm", "15:30"
  • "morning", "afternoon", "evening"
  • "noon", "midnight"

multiSelect() - Multiple Choice Selection

typescript
import { multiSelect } from "@andresaya/flowkit";

flow("preferences", agent)
  .ask(
    "select_toppings",
    "What toppings would you like? (choose multiple)",
    multiSelect(["cheese", "pepperoni", "mushrooms", "olives", "peppers"]),
    "toppings"
  )
  .then("confirm")

Returns an array of selected options:

  • User: "I want cheese and pepperoni"
  • Slot: ["cheese", "pepperoni"]

Confirm Steps

Add confirmation before proceeding (skeleton - full implementation coming).

typescript
flow("order", agent)
  .ask("get_email", "Email?", email(), "email")
  .then("get_address")
  
  .ask("get_address", "Address?", text(), "address")
  .confirm("Is this correct? Email: {{email}}, Address: {{address}}")
  .then("process")

Combining Features

All these features can be combined:

typescript
flow("smart-order", agent)
  .ask("get_email", "What's your email?", email(), "email")
  .transform("lowercase")
  .do(async (ctx) => {
    const user = await db.findByEmail(ctx.slots.email);
    if (user) {
      ctx.slots.has_account = true;
      ctx.slots.saved_address = user.address;
    }
  })
  .then("ask_address")
  
  .ask("ask_address", "Shipping address?", text(), "address")
  .skipIf((ctx) => ctx.slots.saved_address)
  .transform("capitalize")
  .then("ask_express")
  
  .ask("ask_express", "Express shipping (+$10)?", yesNo(), "express")
  .onlyIf((ctx) => ctx.slots.has_account)  // Only VIP customers
  .ifYes("express_checkout")
  .ifNo("standard_checkout")

Released under the MIT License.