# Meeting notes

Published by Croft · Category: Productivity

Meetings with attendees, notes, and action items that have an owner, a due date, and a done flag.

Keeps a record of every meeting and, more importantly, of what everyone agreed to do afterwards. Each meeting carries a date, a time, a location, a purpose, and the people who were in the room, with absentees marked rather than quietly dropped. Notes are captured against the meeting as they happen and can be pinned so the decision does not get buried under the discussion. Action items are the point of the whole thing: each one has an owner, a due date, a priority, and a done flag, and the app surfaces them across every meeting at once so nothing goes stale in a document nobody reopens. Anything past its due date and still open is flagged as overdue, and a By owner view shows who is carrying the most and what they owe next.

Built on four append-only record streams (meetings, attendees, notes, and action_items) with a single callable workflow as the only write path. Every append is a full snapshot of the row, so the newest row per id is the current state and a removal is a tombstone rather than an erasure; that gives an audit trail of how an action item's owner or due date changed over time, for free. The page is one self-contained HTML artifact that reads current state with a row_number window query and writes through the workflow, minting its own row ids so an attendee can reference the meeting created alongside it. Ticking an action item off updates optimistically and then reconciles against the record stream.

For any team that runs recurring meetings and keeps losing the follow-through: a weekly engineering sync, a client onboarding call, a leadership review, a committee that meets monthly. It is aimed at the person who ends up owning the minutes and is tired of chasing commitments across a pile of documents. Install it as-is for a small team, or treat it as the starting point for a meeting workflow with your own fields, since the write engine takes a flat field map and adding a column costs nothing.

## Requirements

Requires no connections. Runs on demand, when called directly.

### Record streams

- action_items (write)
- attendees (write)
- meetings (write)
- notes (write)

## Source

#### `index.ts`

```typescript
import { callable, workflow, z } from "@croft/sdk";

/**
 * Meeting notes — the write engine behind the meeting notes app.
 *
 * Every entity lives in an append-only record stream as an event log: each
 * append is a full snapshot of the row, and the newest row per `id` wins.
 * Removals append a tombstone (`_op: "gone"`) rather than erasing history.
 * The artifact page reads current state with a `row_number()` window query.
 *
 * `_op` is "live"/"gone" and never the word "delete", because `records.query`
 * rejects COPY EXPORT INSTALL LOAD ATTACH CREATE INSERT UPDATE DELETE PRAGMA
 * as whole words anywhere in the SQL text — string literals included.
 *
 * The appends are written out one stream at a time, rather than through a
 * computed stream name, so the analyzer can name every stream this workflow
 * touches and an install plan can list them.
 */

const Input = z.object({
  entity: z
    .enum(["meetings", "attendees", "notes", "action_items"])
    .describe("Which record stream to write"),
  action: z
    .enum(["save", "remove"])
    .describe("save writes a full snapshot of the row; remove appends a tombstone"),
  id: z
    .string()
    .describe("Stable id for the row. Reuse it to update; omit on create to mint one.")
    .optional(),
  data: z
    .record(z.string(), z.any())
    .describe(
      "Flat field map. meetings{title,held_on,starts_at,duration_min,location,purpose}; attendees{meeting_id,person,role,present}; notes{meeting_id,body,author,pinned}; action_items{meeting_id,title,owner,due_on,done,priority}",
    )
    .optional(),
});

export default workflow(
  "meeting-notes",
  {
    trigger: callable({
      description:
        "Create, update, or remove a meeting, an attendee, a note, or an action item in the meeting notes app.",
      input: Input,
    }),
    connections: [],
    description:
      "Write engine for the meeting notes app: meetings with attendees and running notes, plus action items that carry an owner, a due date, and a done flag.",
  },
  async (ctx) => {
    const { entity, action, id, data } = ctx.input;

    const row = await ctx.step("Build row", () => {
      const rowId = id ?? `${entity.slice(0, 3)}_${Math.random().toString(36).slice(2, 10)}`;
      return {
        ...(action === "remove" ? {} : (data ?? {})),
        id: rowId,
        _op: action === "remove" ? "gone" : "live",
        _ts: new Date().toISOString(),
      };
    });

    if (entity === "meetings") {
      await ctx.records.append("meetings", [row]);
    } else if (entity === "attendees") {
      await ctx.records.append("attendees", [row]);
    } else if (entity === "notes") {
      await ctx.records.append("notes", [row]);
    } else {
      await ctx.records.append("action_items", [row]);
    }

    return { id: row.id, entity, action, ts: row._ts };
  },
);

```

## Install

Install this blueprint: https://croft.now/blueprints/croft-now/meeting-notes/install

---
View online: https://croft.now/blueprints/croft-now/meeting-notes
