Developers · Go

Make a meeting bot speak, from Go

A Go program that says something in the meeting, with what it printed when it was run.

Ran on 10 November 2026Language GoJob Speak

What it does

After agent-ready, the program writes one say message with wsjson.Write and waits for agent-said, which confirms the words went into the meeting.

It needs a key and a meeting code in MEETFUL_KEY and MEETING. Two curl requests make both. MEETFUL_URL is optional and defaults to https://meetful.com.

Go 1.22 or later. The socket programs use github.com/coder/websocket (go get github.com/coder/websocket@v1.8.12); the transcript program uses only the standard library.

The code

// Say something in a Meetful meeting from Go. go get github.com/coder/websocket
// MEETFUL_KEY=tmk_… MEETING=abc-defg-hij go run . "Noted: the launch moves to the third of March."
package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"strings"
	"time"

	"github.com/coder/websocket"
	"github.com/coder/websocket/wsjson"
)

type message struct {
	Type, By, Text, Why string
	Audio               any
}

func main() {
	base := os.Getenv("MEETFUL_URL")
	if base == "" {
		base = "https://meetful.com"
	}
	text := "Hello from Go."
	if len(os.Args) > 1 {
		text = os.Args[1]
	}
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()
	conn, _, err := websocket.Dial(ctx, "ws"+strings.TrimPrefix(base, "http"), nil)
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close(websocket.StatusNormalClosure, "done")
	conn.SetReadLimit(4 << 20)

	hello := map[string]string{"type": "agent-hello", "key": os.Getenv("MEETFUL_KEY"), "meeting": os.Getenv("MEETING")}
	if err := wsjson.Write(ctx, conn, hello); err != nil {
		log.Fatal(err)
	}
	for {
		var m message
		if err := wsjson.Read(ctx, conn, &m); err != nil {
			log.Fatal(err)
		}
		switch m.Type {
		case "agent-ready":
			if err := wsjson.Write(ctx, conn, map[string]string{"type": "say", "text": text}); err != nil {
				log.Fatal(err)
			}
		case "agent-said":
			aloud := ""
			if m.Audio != nil {
				aloud = " (read aloud)"
			}
			fmt.Printf("%s said: %s%s\n", m.By, m.Text, aloud)
			return
		case "agent-error", "agent-refused":
			log.Fatal(m.Type + ": " + m.Why)
		}
	}
}

Run it

MEETFUL_KEY=tmk_… MEETING=abc-defg-hij go run . "Noted: the launch moves to the third of March, and Sam owns the docs."

What it printed

🤖 go bot said: Noted: the launch moves to the third of March, and Sam owns the docs.

Run on 10 November 2026 against a Meetful server built from the same code as meetful.com, with two people — Dana and Sam — in the meeting saying three lines. Nothing above was edited.

How it works

Audio is typed as any, because it is null when the server has no voice and an object when it has one. The run below was on a server without a voice key, so it printed no "(read aloud)".

coder/websocket lets you write from any goroutine; only reading has to stay in one place. A bot that talks while it listens can keep its read loop as it is and send from wherever its answers are made.

What can go wrong

Refused: no such key — mint one with POST /api/keys

The key is wrong, or was never minted on this server. A key from one Meetful server does not work on another.

agent-error: …

Sent too quickly, or something this key may not do; agent-ready's can lists what it may.

Next

In Go: join a meeting · live transcript · get the transcript

The same in Node.js · Python · curl

The whole API · Every socket message and webhook · All guides

Try Meetful

Meetings that take their own notes

Free video meetings in the browser: every line of the transcript under the name of who said it, and the summary and action items when you hang up. Nothing to install — guests just open the link.