Skip to content

SDK

Use the Flora TypeScript SDK to build bot scripts that run inside the Flora runtime. In the runtime, SDK functions are available globally (no imports needed).

Quickstart

ts
const ping = prefix({
  name: 'ping',
  description: 'Respond with pong',
  run: async (ctx) => {
    const extra = ctx.args.join(' ') || 'none'
    await ctx.reply(`pong! args: ${extra}`)
  }
})

createBot({
  prefix: '!',
  commands: [ping]
})

Event handlers

ts
on('ready', async (ctx) => {
  console.log('bot ready')
})

on('messageCreate', async (ctx) => {
  if (ctx.msg.author?.bot) return
  if (ctx.msg.content === '!hello') {
    await ctx.reply('hello!')
  }
})

on('messageUpdate', async (ctx) => {
  console.log('message updated', ctx.msg.id)
})

on('messageDelete', async (ctx) => {
  console.log('message deleted', ctx.msg.id)
})

on('messageDeleteBulk', async (ctx) => {
  console.log('bulk delete', ctx.msg.ids.length)
})

on('interactionCreate', async (ctx) => {
  console.log('interaction', ctx.msg.commandName)
})

Prefix commands

ts
const greet = prefix({
  name: 'greet',
  description: 'Greet someone',
  run: async (ctx) => {
    const name = ctx.args[0] || 'world'
    await ctx.reply(`Hello, ${name}!`)
  }
})

createBot({
  prefix: '!',
  commands: [greet]
})

Slash commands

ts
const echo = slash({
  name: 'echo',
  description: 'Echo your input',
  options: [
    { name: 'text', description: 'Text to echo', type: 'string', required: true }
  ],
  run: async (ctx) => {
    const text = ctx.options.text as string
    await ctx.reply({ content: text, ephemeral: true })
  }
})

createBot({
  slashCommands: [echo]
})

Slash command subcommands

ts
const settings = slash({
  name: 'settings',
  description: 'Manage settings',
  subcommands: [
    {
      name: 'get',
      description: 'Get a setting',
      options: [{ name: 'key', description: 'Setting key', type: 'string', required: true }],
      run: async (ctx) => {
        const key = ctx.options.key as string
        await ctx.reply(`Setting ${key}: ...`)
      }
    },
    {
      name: 'set',
      description: 'Set a setting',
      options: [
        { name: 'key', description: 'Setting key', type: 'string', required: true },
        { name: 'value', description: 'Setting value', type: 'string', required: true }
      ],
      run: async (ctx) => {
        const { key, value } = ctx.options as { key: string; value: string }
        await ctx.reply(`Set ${key} = ${value}`)
      }
    }
  ]
})

createBot({ slashCommands: [settings] })

Replies and edits

ts
await ctx.reply('simple reply')

await ctx.reply({
  content: 'rich reply',
  ephemeral: true,
  allowedMentions: { parse: ['users'] }
})

await ctx.edit('edited content')

Embeds

ts
const status = embed()
  .setTitle('Status')
  .setDescription('All systems nominal')
  .setColor(0x00ff00)
  .addField('Uptime', '99.9%', true)
  .addField('Latency', '42ms', true)
  .setFooter('Flora')
  .toJSON()

await ctx.reply({ embeds: [status] })

Components

ts
const row = actionRow().addComponents(
  button().setCustomId('ping').setLabel('Ping').setStyle(ButtonStyle.Primary),
  button().setUrl('https://example.com').setLabel('Docs')
)

await ctx.reply({ content: 'Pick one', components: [row.toJSON()] })

V2 components require a message flag:

ts
const card = container()
  .setAccentColor(0x3366ff)
  .addComponents(
    section()
      .addComponents(textDisplay('Flora runtime'))
      .setAccessory(thumbnail('https://example.com/logo.png'))
  )

await ctx.reply({
  components: [card.toJSON()],
  flags: MessageFlags.IS_COMPONENTS_V2
})

KV store

ts
const store = kv.store('settings')

await store.set('prefix', '!')
const prefixValue = await store.get('prefix')

await store.set('session', JSON.stringify({ userId: '123' }), {
  expiration: Math.floor(Date.now() / 1000) + 3600,
  metadata: { source: 'login' }
})

const { value, metadata } = await store.getWithMetadata('session')

const page = await store.list({ prefix: 'user:', limit: 100 })
if (!page.list_complete) {
  await store.list({ cursor: page.cursor })
}

Cron jobs

Schedule tasks to run at fixed times or intervals using cron expressions. All times are evaluated in UTC.

ts
// Run every minute
cron('heartbeat', '* * * * *', (ctx) => {
  console.log(`Heartbeat at ${ctx.scheduledAt}`)
})

// Run daily at midnight UTC
cron('daily-cleanup', '0 0 * * *', async (ctx) => {
  console.log(`Running ${ctx.name}`)
  // cleanup logic here
})

// Run every hour at minute 30
cron('hourly-report', '30 * * * *', (ctx) => {
  console.log('Generating hourly report')
})

// Run at 9am on weekdays
cron('weekday-reminder', '0 9 * * 1-5', (ctx) => {
  console.log('Good morning!')
})

The cron context provides:

  • name: The cron job name you specified
  • scheduledAt: ISO 8601 timestamp of when the job was scheduled to run

Options

ts
// Skip execution if previous run is still active
cron('long-task', '*/5 * * * *', async (ctx) => {
  await someLongRunningTask()
}, { skipIfRunning: true })
  • skipIfRunning: If true, the job won't start a new execution if the previous one is still running. Default: false.

Cron expressions follow standard 5-field format: minute hour day-of-month month day-of-week.

Limits:

  • Max cron jobs per guild: 32 (configurable via max_cron_jobs)
  • Handler timeout: 5 seconds (configurable via cron_timeout_secs)

Utilities

ts
if (hasRole(ctx, '123456789')) {
  await ctx.reply('role ok')
}

const sub = getSubcommand(ctx)
const group = getSubcommandGroup(ctx)

Types

Types are generated from the Rust runtime and available in the SDK. Use these when authoring bot scripts and tests outside of the runtime bundle.