Skip to content

Getting Started

There is no single "getting started" — it depends on what you want to do. Pick your path:

I want to... Go to
Use MEADOWS (connect to a running server) Using MEADOWS
Host my own server Self-hosting
Develop the server locally Server development
Write a bot Bot development
Investigate the data Data investigation

1. Using MEADOWS

Someone hosts a MEADOWS server for you. You want to chat, interact with bots, maybe fill in a form. You don't need to install anything — just a browser.

What is a JWT?

A JWT (JSON Web Token) is your digital identity. It's a long string that says "I am user alice" (or "I am bot echo"). The server uses it to know who you are and what you're allowed to do.

Think of it like a wristband at a festival: you get it at the entrance, it identifies you for the whole event, and it expires when the event ends.

Getting a token

There are two ways to get a token:

Via the web interface. If you have the right permissions, you can generate tokens directly in the chat UI. Look for the token generation buttons in the sidebar or settings panel. You can create: - A user token for yourself (if you have user-invite permission) - A bot token for a bot you want to run (if you have bot-invite permission)

Via the server operator. If you don't have permission to generate tokens yourself, ask the server operator. They generate one with:

cd meadows-server
uv run edwh local.user-jwt --name=yourname

They'll give you a long string like eyJhbGciOiJIUzI1NiIs.... Copy it.

Connecting via web browser

  1. Open the server URL (the operator will tell you, e.g. https://chat.example.com)
  2. Paste your JWT token in the login field
  3. Click "Connect"

You're in. You'll see groups on the left, messages in the center, and a list of available bots.

Connecting via terminal (TUI)

If you prefer a terminal interface:

pip install meadows-tui
MEADOWS_JWT=<your-token> meadows-tui --server https://chat.example.com

Or with more options:

meadows-tui --server https://chat.example.com --token eyJ... --theme dark

Talking to bots

Type @botname command in the message box. For example:

  • @echo hello — the echo bot repeats your message
  • @todo add — the todo bot shows a form to add a task
  • @help — shows available bots and commands

Bots respond in the same conversation. You see their responses alongside messages from other humans.


2. Self-hosting

You want to run your own MEADOWS server. Two options: Docker (simplest) or bare metal.

git clone https://github.com/remcoboerma/meadows-server.git
cd meadows-server

# Set up environment variables
uv run edwh local.setup

# Start with Docker Compose
docker compose up -d

The server starts on port 8080. Traefik handles TLS if configured.

See the Docker guide for production configuration, Traefik labels, and multi-service setup.

Option B: Bare metal (no Docker)

git clone https://github.com/remcoboerma/meadows-server.git
cd meadows-server

# Install with uv
uv pip install -e ".[dev]"
uv pip install edwh

# Generate shared JWT key
uv run edwh local.setup

# Start the server
uv run python -m meadows.server

The server starts on http://localhost:8080.

Generate tokens for your users

# User token (for humans connecting via browser or TUI)
uv run edwh local.user-jwt --name=alice

# Bot token (for automated agents)
uv run edwh local.bot-jwt --name=echo

Share the user tokens with your users. They paste them in the web UI.

Alternatively, grant user-invite and/or bot-invite permissions to trusted users so they can generate their own tokens directly through the web interface — no CLI access needed. This is useful in hackathon settings where participants need to create their own bots.

Serve the web frontend

The web frontend is a separate package:

cd meadows-web
uv pip install -e ".[dev]"

# Build the template (required before serving)
uv run python -m meadows.web.build

# Start the web host
uv run python -m meadows.web

The web UI starts on port 8081. It serves static files — the browser connects directly to the server on port 8080 via Socket.IO.


3. Server development

You want to work on the server code itself — fix bugs, add features, understand the architecture.

Prerequisites

  • Python >= 3.12
  • uv package manager
  • edwh task runner (uv pip install edwh)

Setup

git clone https://github.com/remcoboerma/meadows-server.git
cd meadows-server

# Install in editable mode with dev dependencies
uv pip install -e ".[dev]"
uv pip install edwh

# Generate shared JWT key and check environment
uv run edwh local.setup

Common commands

# Run tests
uv run edwh local.test     # or: uv run pytest -q

# Lint
uv run edwh local.lint      # or: uv run ruff check src tests

# Format
uv run edwh local.fmt       # or: uv run ruff format src tests

# Run the server
uv run python -m meadows.server

# Generate tokens
uv run edwh local.user-jwt --name=alice
uv run edwh local.bot-jwt --name=echo
uv run edwh local.permissions-list

Code map

Run uv run edwh codemap to get a structured overview of every source and test file. Pipe to rg to search:

uv run edwh codemap | rg "on_form_submission"
uv run edwh codemap | rg "BUSINESS RULE"

Architecture

The server is in meadows-server/src/meadows/server/. Key files:

File Purpose
namespace.py Socket.IO event handlers (the main dispatch logic)
hub.py State container: sessions, bots, groups, patterns, persistence
persistence.py JSONL message storage
chokepoint.py Protocol validation before emitting to clients
label_evaluator.py JSON Logic predicate evaluation against labels
label_dedup.py SQLite-backed dedup index for labels

See the Architecture Overview for the full design.


4. Bot development

Someone hosts a MEADOWS server. You have a user JWT and permission to generate bot JWTs. You want to write a bot.

Prerequisites

  • Python >= 3.12
  • uv package manager
  • A user JWT (from the server operator or via the web interface)
  • Permission to generate bot JWTs (bot-invite permission)

1. Clone the bot SDK

git clone https://github.com/remcoboerma/meadows-bot.git
cd meadows-bot
uv pip install -e ".[dev]"

2. Generate a bot token

If you have bot-invite permission, you can generate a bot token directly in the web interface. Otherwise, ask the server operator or use the CLI:

cd meadows-server
uv run edwh local.bot-jwt --name=mybot

Copy the printed token. This is your bot's identity.

3. Write your bot

Create my_bot.py:

from meadows.bot import BaseBot

class MyBot(BaseBot):
    BOT_NAME = "mybot"
    BOT_DESCRIPTION = "A bot that greets people"
    BOT_COMMANDS = [
        {"name": "greet", "description": "Say hello"},
        {"name": "help", "description": "Show commands"},
    ]

    def should_handle(self, command, args):
        return command in {"greet", "help"}

    def handle(self, command, args, raw_args, message, thread_context):
        if command == "greet":
            name = args[0] if args else "friend"
            return f"Hello {name}! I'm {self.BOT_NAME}."
        if command == "help":
            return self.format_help_response()
        return None

if __name__ == "__main__":
    MyBot().connect()

4. Run it

MEADOWS_JWT_TOKEN=<your-bot-token> uv run python my_bot.py

The bot connects to http://localhost:8080 by default. Set MEADOWS_SERVER_URL to change:

MEADOWS_SERVER_URL=https://chat.example.com MEADOWS_JWT_TOKEN=<token> uv run python my_bot.py

5. Try the examples

The SDK includes example bots that demonstrate every feature:

# Simple echo
MEADOWS_JWT_TOKEN=<token> uv run python -m meadows.bot.examples.echo_bot

# Forms (interactive HTML)
MEADOWS_JWT_TOKEN=<token> uv run python -m meadows.bot.examples.todo_bot

# Labels (sentiment analysis)
MEADOWS_JWT_TOKEN=<token> uv run python -m meadows.bot.examples.sentiment_bot

# RPC (bot-to-bot service calls)
MEADOWS_JWT_TOKEN=<token> uv run python -m meadows.bot.examples.math_service_bot

What bots can do

Feature How Docs
Respond to commands should_handle() + handle() Bot SDK
Send interactive forms send_form() Forms
Subscribe to labels register_label_subscription() Labeling
Call other bots call_rpc() RPC
Produce labels on messages emit_label() Labeling
React to patterns register_pattern() + on_pattern_matched() Bot SDK

The bot-author contract

A working bot is four things:

  1. BOT_NAME — your bot's identity
  2. should_handle(command, args) — decide if you handle this command
  3. handle(command, args, raw_args, message, thread_context) — produce a response
  4. connect() — start

Everything else — auth, reconnect, routing, labels, RPC — is handled by the SDK.


5. Data investigation

You want to explore the data: who talked to which bot, what forms were submitted, how labels flow through the system. The data lives in JSONL files on the server.

Where the data is

Messages are stored in meadows-server/messages/<group>.jsonl. Each line is a JSON object representing one message, reaction, label assignment, or form submission.

cd meadows-server/messages

# List all groups
ls *.jsonl

# See recent messages in a group
tail -5 general.jsonl | python -m json.tool

# Count messages per group
wc -l *.jsonl

Message structure

Each line in a JSONL file is a wire-format message:

{
  "id": "1783709286616-39cbbf9d2614",
  "type": "user",
  "user_id": "user-alice",
  "group_id": "general",
  "content": "Hello everyone!",
  "timestamp": "2026-07-10T18:42:00Z",
  "labels": [["meadows", "room:general", "1.0.0"]]
}

Bot messages have "type": "bot" and a "bot_name" field. Form submissions have "type": "form_submission" with response data in metadata.meadows.form_handling.response.

Exploring with jq

jq is your best friend for JSONL exploration:

# All messages from a specific user
cat general.jsonl | jq 'select(.user_id == "user-alice")'

# All bot messages
cat general.jsonl | jq 'select(.type == "bot")'

# All form submissions
cat general.jsonl | jq 'select(.type == "form_submission")'

# All messages with labels
cat general.jsonl | jq 'select(.labels | length > 0)'

# Messages from a specific bot
cat general.jsonl | jq 'select(.bot_name == "todo")'

# Extract form response data
cat general.jsonl | jq 'select(.type == "form_submission") | .metadata.meadows.form_handling.response'

# Count messages per user
cat general.jsonl | jq -r '.user_id // .bot_name // "unknown"' | sort | uniq -c | sort -rn

# Timeline of a conversation
cat general.jsonl | jq '{time: .timestamp, user: (.user_id // .bot_name), content: .content[:80]}'

Label analysis

Labels are stored on messages and also as separate LABEL_ASSIGNED records:

# All label assignments
cat general.jsonl | jq 'select(.event == "label_assigned")'

# Which bots produce which labels
cat general.jsonl | jq 'select(.event == "label_assigned") | {by: .applied_by, labels: [.labels[] | .label]}'

# Messages with the interactive-form label
cat general.jsonl | jq 'select(.labels? // [] | map(select(.[1] == "interactive-form")) | length > 0)'

Programmatic access

For more complex analysis, use Python:

import json

with open("messages/general.jsonl") as f:
    messages = [json.loads(line) for line in f if line.strip()]

# All form submissions
submissions = [m for m in messages if m.get("type") == "form_submission"]

# All bot responses
bot_msgs = [m for m in messages if m.get("type") == "bot"]

# Messages with labels
labeled = [m for m in messages if m.get("labels")]

Dedup index

The server maintains a SQLite-backed dedup index at messages/.label_dedup/. This tracks which (origin, label, semver, message_id) combinations have been seen, preventing duplicate label delivery.


Next steps