FlowOtter gives every person on your team their own persistent AI workspace — scoped to their identity, inaccessible to anyone else. Trigger tasks from Slack, store the results, and recall them wherever you work next.
In a shared environment like Slack, anyone can see a thread — but only you can add it to your memory. No one else can write to your workspace, read from it, or pollute it. It's yours.
Bot name: Flo · Phase: 1 (Slack + SSO + Storage + Retrieval)
@Flo <instruction>— Execute any natural language task. Flo runs it through your configured LLM and stores the full output as markdown in S3, scoped to your identity.@Flo what did you find about X?— Retrieve past sessions semantically. Flo searches your history and synthesises a response with source dates.- Proactive tasks:
@Flo pull the open customer complaints for Acme and DM me 15 minutes before my 3pm call— schedule work and get results delivered at the right moment. - Capture from shared spaces: Spot a useful Slack discussion?
@Flo add this thread to my memory— it's stored in your personal workspace, invisible to others, and retrievable later wherever you work. - Works in channels, threads, and DMs.
- Each member's workspace is fully isolated — no one else can read from it, write to it, or inject into it.
- Docker + Docker Compose
- A Slack workspace where you can install apps
- An Anthropic or OpenAI API key (for LLM execution)
- An OpenAI API key (always required for embeddings, even if using Anthropic for LLM)
- A Google Workspace OAuth 2.0 app (or any OIDC provider)
- An AWS account with an S3 bucket (or use LocalStack for fully local dev)
ngrokor equivalent to expose your local port to Slack (development only)
git clone https://github.com/your-org/flowotter.git
cd flowotter
npm installcp .env.example .envEdit .env with your credentials. See the inline comments for each variable.
Minimum required for local dev:
SLACK_BOT_TOKEN,SLACK_SIGNING_SECRETFLOWOTTER_LLM_PROVIDER,FLOWOTTER_LLM_API_KEYOPENAI_API_KEY(for embeddings)FLOWOTTER_OIDC_CLIENT_ID,FLOWOTTER_OIDC_CLIENT_SECRET,FLOWOTTER_OIDC_REDIRECT_URIFLOWOTTER_BASE_URL(your ngrok URL once running)
- Go to api.slack.com/apps → Create New App → From manifest
- Paste the following manifest:
display_information:
name: Flo
description: Your persistent AI work assistant
features:
bot_user:
display_name: Flo
always_online: true
oauth_config:
scopes:
bot:
- app_mentions:read
- chat:write
- channels:history
- im:history
- im:write
- im:read
- users:read
settings:
event_subscriptions:
bot_events:
- app_mention
- message.im
interactivity:
is_enabled: false
org_deploy_enabled: false
socket_mode_enabled: false- Install the app to your workspace and copy the Bot User OAuth Token →
SLACK_BOT_TOKEN - From Basic Information, copy the Signing Secret →
SLACK_SIGNING_SECRET
- Go to console.cloud.google.com → APIs & Services → Credentials
- Click Create Credentials → OAuth 2.0 Client ID → Web application
- Add
http://localhost:3000/auth/callbackto Authorized redirect URIs - Copy the Client ID and Client Secret to
.env
docker compose upThis starts:
- FlowOtter on port
3000 - Postgres (with pgvector) on port
5432 - LocalStack S3 on port
4566
Database migrations run automatically on startup.
ngrok http 3000Copy the https://...ngrok.io URL and:
- Update
FLOWOTTER_BASE_URLin.envto the ngrok URL - Update
FLOWOTTER_OIDC_REDIRECT_URItohttps://...ngrok.io/auth/callback - Update the Slack app's Event Subscriptions Request URL to
https://...ngrok.io/slack/events - Update the Authorized redirect URIs in Google Cloud Console
Restart docker compose after updating .env.
- Add Flo to a Slack channel
- Send
@Flo hello - Flo should DM you with an authentication link
- Click the link, complete Google SSO
- Send
@Flo helloagain — Flo should respond normally
docker build -t flowotter:latest .
docker tag flowotter:latest <your-ecr-repo>/flowotter:latest
docker push <your-ecr-repo>/flowotter:latestSet all variables from .env.example as ECS task environment variables or Secrets Manager references.
For production:
FLOWOTTER_STORAGE_PROVIDER=s3with a real S3 bucketFLOWOTTER_BASE_URLset to your production domainDATABASE_URLpointing to an RDS Postgres instance with the pgvector extension enabled- Remove
FLOWOTTER_S3_ENDPOINT(use native S3) - Use an ECS task role with
s3:PutObject,s3:GetObjectpermissions instead ofAWS_ACCESS_KEY_ID
CREATE EXTENSION IF NOT EXISTS vector;Run this once on your RDS instance before the first deployment. RDS for Postgres supports pgvector on Postgres 15+.
┌─────────────────────┐
│ Slack Event │
│ (DM or @mention) │
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ Bolt App — ack() │
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ Identity Middleware │
│ (lookup Slack ID) │
└──────────┬──────────┘
│
┌──────────────┴──────────────┐
User found? Not found
│ │
│ ┌──────────▼──────────┐
│ │ Send auth DM with │
│ │ Google OIDC link │
│ └──────────┬──────────┘
│ │
│ ┌──────────▼──────────┐
│ │ User authenticates │
│ │ OAuth callback → │
│ │ create user record │
│ └─────────────────────┘
│
┌──────────▼──────────┐
│ classifyIntent() │
│ (LLM call) │
└──────────┬──────────┘
│
┌─────────┴──────────┐
TASK RETRIEVE
│ │
│ ┌───────▼──────────────┐
│ │ Acknowledge in thread │
│ │ "Searching history..." │
│ └───────┬──────────────┘
│ │
│ ┌───────▼──────────────┐
│ │ Generate query embed │
│ │ (OpenAI 3-small) │
│ └───────┬──────────────┘
│ │
│ ┌───────▼──────────────┐
│ │ pgvector search │
│ │ cosine×0.7 + │
│ │ recency×0.3 │
│ └───────┬──────────────┘
│ │
│ ┌─────────┴──────────────┐
│ No results Results found
│ │ │
│ ┌──────▼──────┐ ┌─────────▼──────────┐
│ │ Reply: │ │ Fetch top 3 │
│ │ nothing found│ │ previews from S3 │
│ └─────────────┘ └─────────┬──────────┘
│ │
│ ┌──────────▼──────────┐
│ │ LLM synthesise from │
│ │ excerpts │
│ └──────────┬──────────┘
│ │
│ ┌──────────▼──────────┐
│ │ Reply in thread │
│ │ with source dates │
│ └─────────────────────┘
│
┌────────▼─────────────┐
│ Acknowledge in thread │
│ "On it..." │
└────────┬─────────────┘
│
┌────────▼─────────────┐
│ Fetch thread context │
│ from Slack (up to 20) │
└────────┬─────────────┘
│
┌────────▼─────────────┐
│ Build system prompt │
│ + LLM complete() │
└────────┬─────────────┘
│
┌────────▼─────────────┐
│ Format session │
│ markdown │
└────────┬─────────────┘
│
┌────────▼─────────────┐
│ Store to S3 │
│ (user-namespaced key) │
└────────┬─────────────┘
│
┌────────▼─────────────┐
│ Reply in Slack thread │ ◄── user sees response here
└────────┬─────────────┘
│
┌────────▼─────────────┐
│ Generate embedding │
│ (OpenAI 3-small) │
└────────┬─────────────┘
│
┌────────▼─────────────┐
│ Insert session row │
│ to PostgreSQL │
│ (with vector) │
└───────────────────────┘
- Per-user data isolation: Every S3 key and every database query is scoped to the authenticated user's ID. A user cannot access another user's data.
- Opaque S3 prefixes: Each user is assigned a random
usr_{id}prefix at registration. Email and SSO sub are never used in S3 paths. - Embeddings always use OpenAI:
text-embedding-3-small(1536 dimensions) is used for all semantic indexing, regardless of which LLM provider handles task execution. This ensures a consistent vector space. - Auth state in Postgres: OAuth2 state tokens are stored in the database, not in memory. This makes the service stateless and safe to run as multiple containers.
| Test | Description |
|---|---|
| T-01 | First-time user receives auth DM, authenticates, can use Flo |
| T-02 | Unauthenticated user is blocked and sent auth DM |
| T-03 | Task execution stores markdown at correct S3 path |
| T-04 | @Flo summarise this thread uses thread context |
| T-05 | Retrieval returns relevant past session |
| T-06 | More recent sessions rank higher when relevance is equal |
| T-07 | No-results retrieval offers to research instead |
| T-08 | LLM error surfaces as DM, service stays up |
| T-09 | User B cannot retrieve User A's sessions |
| T-10 | DM invocation works end-to-end |
FlowOtter is open source (MIT). Issues and PRs welcome.
MIT — see LICENSE.