Work
Project management: projects, epics, deliveries, tasks, business units, and member placement against the work schema.
| Endpoint | https://work.mcp.devfellowship.com/mcp |
|---|---|
| Tools | 35 in 9 groups |
| Package | packages/dfl-mcp-work |
| Auth |
Your dfl-auth login token as Authorization: Bearer <token>. Every call runs as
you, under RLS. See Auth & security.
|
{ "mcpServers": { "dfl-work": { "type": "http", "url": "https://work.mcp.devfellowship.com/mcp", "headers": { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } } }}Other clients (Cursor, VS Code, codex, the Anthropic SDK): see Getting started, step 3.
| Backing data | work schema (projects, epics, deliveries, tasks, business units, placements). |
Projects
Section titled “Projects”Find, create and edit projects.
list_projects
List all projects with optional filters.
list_projectsList Projects
List all projects with optional filters.
| Parameter | Type | Required | Description |
|---|---|---|---|
limit | number | no | Maximum number of projects to return (default: 50, max: 100) |
offset | number | no | Number of projects to skip (for pagination) |
business_unit_id | string | no | Filter by business unit ID. |
search | string | no | Search by project name. |
get_project
Get a specific project by ID with its epics.
get_projectGet Project
Get a specific project by ID with its epics.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | The UUID of the project. |
create_project
Create a new project.
create_projectCreate Project
Create a new project.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | yes | Project name. |
business_unit_id | string | no | Business unit ID. |
update_project
Update an existing project.
update_projectUpdate Project
Update an existing project.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | The UUID of the project to update. |
name | string | no | Project name. |
business_unit_id | string | no | Business unit ID. |
delete_project
Delete a project. This will fail if the project has associated epics.
delete_projectDelete Project
Delete a project. This will fail if the project has associated epics.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | The UUID of the project to delete. |
The epics of a project, with their deliveries and tasks.
list_epics
List all epics with optional filters.
list_epicsList Epics
List all epics with optional filters.
| Parameter | Type | Required | Description |
|---|---|---|---|
limit | number | no | Maximum number of epics to return (default: 50, max: 100) |
offset | number | no | Number of epics to skip (for pagination) |
project_id | string | no | Filter by project ID. |
status | enum | no | Filter by status. One of: pending, in_progress, done, no_longer_needed. |
search | string | no | Search by epic name. |
get_epic
Get a specific epic by ID with its deliveries and tasks.
get_epicGet Epic
Get a specific epic by ID with its deliveries and tasks.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | The UUID of the epic. |
create_epic
Create a new epic. 🔴 AN EPIC CANNOT BE SHOWN ON A PLAN — do not tell anyone you linked one.
create_epicCreate Epic
Create a new epic. 🔴 AN EPIC CANNOT BE SHOWN ON A PLAN — do not tell anyone you linked one. The plans-app renders bound TASKS only (it reads work.entity_connections filtered to target_type='task'), so an epic connection would be a row nothing displays. What to do instead: create the epic here, create its tasks with create_task, then bind THOSE tasks to the plan with set_plan_tasks on the PLANS MCP (plans.mcp.devfellowship.com) — that is what makes the work visible on the plan. Name the epic in the plan body if a reader needs to know which epic holds the tasks.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | yes | Epic name. |
project_id | string | no | Project ID. |
notes | string | no | Epic notes/description. |
status | enum | no | Epic status. One of: pending, in_progress, done, no_longer_needed. |
is_long_lived | boolean | no | Whether this is a long-lived epic. |
update_epic
Update an existing epic.
update_epicUpdate Epic
Update an existing epic.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | The UUID of the epic to update. |
name | string | no | Epic name. |
project_id | string | no | Project ID. |
notes | string | no | Epic notes/description. |
status | enum | no | Epic status. One of: pending, in_progress, done, no_longer_needed. |
is_long_lived | boolean | no | Whether this is a long-lived epic. |
delete_epic
Delete an epic. This will fail if the epic has associated deliveries or tasks.
delete_epicDelete Epic
Delete an epic. This will fail if the epic has associated deliveries or tasks.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | The UUID of the epic to delete. |
Deliveries
Section titled “Deliveries”The deliveries of an epic, with their tasks.
list_deliveries
List all deliveries with optional filters.
list_deliveriesList Deliveries
List all deliveries with optional filters.
| Parameter | Type | Required | Description |
|---|---|---|---|
limit | number | no | Maximum number of results (default 50) |
offset | number | no | Number of results to skip. |
epic_id | string | no | Filter by epic ID. |
owner_id | string | no | Filter by owner (member) ID. |
status | enum | no | Filter by status. One of: pending, in_progress, completed, canceled, no_longer_needed. |
search | string | no | Search in name and notes. |
get_delivery
Get a specific delivery by ID with its epic and tasks.
get_deliveryGet Delivery
Get a specific delivery by ID with its epic and tasks.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | The UUID of the delivery. |
create_delivery
Create a new delivery.
create_deliveryCreate Delivery
Create a new delivery.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | yes | Delivery name. |
notes | string | no | Delivery notes. |
epic_id | string | yes | Epic ID (required) |
owner_id | string | no | Owner (member) ID. |
status | enum | no | Delivery status. One of: pending, in_progress, completed, canceled, no_longer_needed. |
price | number | no | Delivery price. |
price_per_point | number | no | Price per story point. |
total_points | number | no | Total story points. |
transaction_id | string | no | Transaction ID. |
update_delivery
Update an existing delivery.
update_deliveryUpdate Delivery
Update an existing delivery.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | The UUID of the delivery to update. |
name | string | no | Delivery name. |
notes | string | no | Delivery notes. |
epic_id | string | no | Epic ID. |
owner_id | string | no | Owner (member) ID. |
status | enum | no | Delivery status. One of: pending, in_progress, completed, canceled, no_longer_needed. |
price | number | no | Delivery price. |
price_per_point | number | no | Price per story point. |
total_points | number | no | Total story points. |
number_of_tasks | number | no | Number of tasks. |
number_of_completed_tasks | number | no | Number of completed tasks. |
transaction_id | string | no | Transaction ID. |
delete_delivery
Delete a delivery.
delete_deliveryDelete Delivery
Delete a delivery.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | The UUID of the delivery to delete. |
Find tasks by owner, status, epic or delivery; each task carries its estimated date, so you can see who is late and what is overdue. Get the Git branch name of a task.
list_tasks
List all tasks with optional filters.
list_tasksList Tasks
List all tasks with optional filters. Each task row carries its owner_id, status and estimated_date (the planned completion date). To answer "who is late" or "which tasks are overdue", list the open tasks (status not done / no_longer_needed) and keep the rows whose estimated_date is before today. There is no overdue filter: compare the dates yourself.
| Parameter | Type | Required | Description |
|---|---|---|---|
limit | number | no | Maximum number of tasks to return (default: 50, max: 100) |
offset | number | no | Number of tasks to skip (for pagination) |
epic_id | string | no | Filter by epic ID. |
delivery_id | string | no | Filter by delivery ID. |
owner_id | string | no | Filter by owner (member) ID. |
status | enum | no | Filter by status. One of: to_do, in_progress, dev_completed, done, no_longer_needed, blocked. |
search | string | no | Search by task name. |
get_task
Get a specific task by ID with its epic and delivery.
get_taskGet Task
Get a specific task by ID with its epic and delivery.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | The UUID of the task. |
create_task
Create a new task. Requires a context package (why/what) so the task is readable by whoever was not in the conversation it came from.
create_taskCreate Task
Create a new task. Requires a context package (why/what) so the task is readable by whoever was not in the conversation it came from. 🔴 IF THIS TASK BELONGS TO A PLAN, CREATING IT IS ONLY HALF THE JOB. This tool writes work.tasks and NOTHING ELSE — it does not attach the task to a plan, and no tool on this server does. A plan renders its execution checklist from work.entity_connections, so the task stays invisible to the plan until you call set_plan_tasks on the PLANS MCP (plans.mcp.devfellowship.com) with the plan slug and EVERY task id the plan should show: set_plan_tasks({ slug, tasks: [{ task_id }, ...] }). It REPLACES the plan's task set, so pass the full desired list, not just the new ids. The failure mode is silent and has already happened: 18 tasks created under an epic for a plan, reported as linked, and the plan page showed nothing (2026-08-12). A created task is a complete, valid row on its own, so there is no error to notice — the only signal is an empty rail on the plan, which reads as "no work started". Verify by reading the plan back and seeing your tasks in its links.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | yes | Task name. |
description | string | no | Task description. |
epic_id | string | no | Epic ID. |
delivery_id | string | no | Delivery ID. |
owner_id | string | no | Owner (public.members.id). Omitir atribui a task a quem chamou a tool. Uma task sem dono some do board, entao nao existe caminho que grave null aqui. |
status | enum | no | Task status. One of: to_do, in_progress, dev_completed, done, no_longer_needed, blocked. |
points | number | no | Story points. |
priority | number | no | Priority (lower = higher priority) |
estimated_date | string | no | Estimated completion date (ISO format) |
attachments | string[] | no | Array of attachment URLs. |
acceptance_criteria | string[] | no | What has to be true for this task to be done, ONE criterion per array entry. The column is text[], not text — do not pass a single blob with numbered lines, because nothing can then render or check a criterion on its own. Write each entry so it can be verified without asking the author: name the command, the endpoint, or the observation that settles it. |
stage_id | enum | yes | What kind of work this is. Required: a task without a stage is invisible on the kanban board, and 1160 of them accumulated that way while this was optional. This is a DIFFERENT AXIS from status — pick where the work starts, not how far along it is. design (board: Ideation) for exploring references and alternatives, decision (board: Design Review) for a proposal waiting on someone to call it, qa (board: QA / Test) for finish work like copy and edge states, spec for shaping the requirement, execution for work ready to be built, review (board: QA / Test) for final validation of work already built. qa and review share the QA / Test column and are told apart by a badge on the card: qa shows Design, review shows Engineering. One of: spec, design, execution, review, decision, qa. |
context | object | yes | Context package: why the task exists, what was done, and the links. |
actor_slug | string | no | The agent/service slug that DID this work (e.g. 'claude-main'). ABSENCE = human task (owner is the calling member, no actor attribution). PRESENT = the task is attributed to that actor via public.actor_links; the slug MUST resolve to an existing actor or the call is REJECTED and nothing is written. |
request_id | string | no | Optional caller request UUID. Disabled by default. When enabled, retain this UUID and the same input for a safe retry. A missing task returns uncertainty, not recreation. |
update_task
Update an existing task.
update_taskUpdate Task
Update an existing task.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | The UUID of the task to update. |
name | string | no | Task name. |
description | string | no | Task description. |
epic_id | string | no | Epic ID. |
delivery_id | string | no | Delivery ID. |
owner_id | string | no | Owner (member) ID. |
status | enum | no | Task status. One of: to_do, in_progress, dev_completed, done, no_longer_needed, blocked. |
points | number | no | Story points. |
priority | number | no | Priority (lower = higher priority) |
estimated_date | string | no | Estimated completion date (ISO format) |
attachments | string[] | no | Array of attachment URLs. |
acceptance_criteria | string[] | no | What has to be true for this task to be done, ONE criterion per array entry. REPLACES the whole list — send every criterion you want to keep, not only the new one. The column is text[], not text. |
stage_id | enum | no | Stage ID (a task without one is invisible on the kanban board) One of: spec, design, execution, review, decision, qa. |
delete_task
Delete a task.
delete_taskDelete Task
Delete a task.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | The UUID of the task to delete. |
get_task_branch_name
Returns the suggested Git branch name for a task based on its identifier and name.
get_task_branch_nameGet Task Branch Name
Returns the suggested Git branch name for a task based on its identifier and name. Format: feat/DFL-XXXX-slug-of-name.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | The UUID of the task. |
Optional task request UUID
Section titled “Optional task request UUID”create_task accepts an optional request_id UUID. The keyed path defaults to disabled.
A supplied UUID returns not_configured without a write while WORK_CREATE_TASK_REQUEST_IDS is not true.
Calls without request_id keep the existing behavior.
A refusal before a write describes only the current call. An earlier attempt with that UUID can already exist.
Retain the request UUID when an earlier outcome is unknown.
When enabled, retain the UUID and the same accepted task input after a lost response. The server hashes the original validated input before it resolves owner or actor defaults. Object key order does not change the hash. Array order and absent fields do change it. The server normalizes the request UUID to lowercase. It excludes that UUID from the payload hash.
The original public.activity_logs creation snapshot binds the caller, operation, request UUID, and payload hash.
Task edits do not change this binding. A different input returns conflict without a write.
A matching visible task returns success: true with status: reconciled.
A missing or inaccessible task returns uncertain. The server does not recreate it.
Invalid audit proof and unknown insert outcomes also return uncertain.
An explicit database refusal returns rejected when no original binding is visible.
Actor attribution uses a separate request. The original snapshot stores the resolved actor in server-owned context.creation_actor.
A retry uses that identity, even if the slug mapping changes.
The server reconciles the existing unique actor link. It does not overwrite the link or delete the task.
Missing or unverified attribution returns success: false, status: attribution_pending, task_id, and request_id.
This is not a claim of one transaction across task creation and actor attribution.
Clients must retain the request UUID for uncertain and attribution_pending outcomes.
Clients must not treat those outcomes as a completed task operation.
context.source, context.creation_request, and context.creation_actor are server-owned.
Supply only the documented context fields. The keyed path rejects unsupported context fields. Legacy calls retain their previous context filtering.
The creation request metadata contains exactly operation, request_id, and payload_sha256.
Activation gate
Section titled “Activation gate”Set WORK_CREATE_TASK_REQUEST_IDS=true only after all these checks:
- A human merges and applies the
dfl-schemamigration20261006150201_public_activity_logs_creation_requests.sql. - Confirm
activity_logs_creation_request_metadata_check,activity_logs_creation_request_key, and the audit ACL changes exist in production. - Complete the production metadata validity and uniqueness preflight in the schema contract.
- Confirm runtime retention excludes keyed original creation events from every audit purge.
- Prove a real caller fixture can reconcile a lost response without a duplicate task or actor link.
- Prove changed input returns conflict. Prove a deleted task does not return or reappear.
The flag does not detect schema deployment. Local transport tests do not prove production activation. The current service change does not prove a Mac ChatGPT or Codex retry. Plan T6 remains open until the schema, service, and client evidence are complete.
Business Units
Section titled “Business Units”Find, create and edit business units.
list_business_units
List all business units with optional filters.
list_business_unitsList Business Units
List all business units with optional filters. Requires finance/admin/owner role.
| Parameter | Type | Required | Description |
|---|---|---|---|
limit | number | no | Maximum number of business units to return (default: 50, max: 100) |
offset | number | no | Number of business units to skip (for pagination) |
search | string | no | Search by business unit name. |
tag | string | no | Filter by tag. |
get_business_unit
Get a specific business unit by ID.
get_business_unitGet Business Unit
Get a specific business unit by ID. Requires finance/admin/owner role.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | Business unit ID (UUID) |
create_business_unit
Create a new business unit.
create_business_unitCreate Business Unit
Create a new business unit. Requires finance/admin/owner role.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | yes | Business unit name. |
profile_image_url | string | no | Profile image URL. |
tags | string[] | no | Array of tags. |
update_business_unit
Update an existing business unit.
update_business_unitUpdate Business Unit
Update an existing business unit. Requires finance/admin/owner role.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | Business unit ID (UUID) |
name | string | no | Business unit name. |
profile_image_url | string | no | Profile image URL, null to remove. |
tags | string[] | no | Array of tags, null to remove. |
delete_business_unit
Delete a business unit by ID.
delete_business_unitDelete Business Unit
Delete a business unit by ID. Requires finance/admin/owner role.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | Business unit ID (UUID) |
Placements
Section titled “Placements”Record where a fellow got placed (job, freelance, internal), and map a speaker name to a member.
create_placement
Record a DFL fellow placement (job/freelance/internal) in work.placements.
create_placementCreate Placement
Record a DFL fellow placement (job/freelance/internal) in work.placements. Tier A = external employment (vaga em empresa terceira). Tier B = external paid freelance/project. Tier C = internal DFL/Revera paid project. Provide member_id directly, or resolve a fellow name to its public.members.id with lookup_member (in the learn MCP) first. All fields (incl. reason_we_helped, is_internal_hire_promoted, had_prior_tech_background, consent_to_report) are applied in the current prod schema. Runs under the caller JWT — RLS enforces global-admin write access.
| Parameter | Type | Required | Description |
|---|---|---|---|
member_id | string | yes | public.members.id of the fellow (resolve a name via lookup_member in the learn MCP). FK -> public.members(id). |
company | string | yes | Employer / company / org name (column: company). |
role | string | yes | Role / job title (column: role). |
type | enum | no | Kind of placement. DB default: employed. (column: type / work.placement_type) One of: employed, freelance, founded, internship. |
started_at | string | yes | Start date (ISO date, e.g. 2025-03-01). (column: started_at) |
ended_at | string | no | End date (ISO date); omit if still active. (column: ended_at) |
placement_tier | enum | no | A = external employment; B = external paid freelance; C = internal DFL/Revera paid project. Optional (nullable in schema), but should be set for the UNICEF-headline slicing. One of: A, B, C. |
source_of_record | enum | no | Confidence of the record source (low→high). Defaults to self_report (DB default). (column: source_of_record / work.placement_source) One of: self_report, mentor_confirmed, contract_doc, employer_confirmed. |
reason_we_helped | enum[] | no | How DFL helped the fellow land this placement (enum array). ⚠️ Requires pending migration dfl-schema #397 — omit until applied or the insert will error. |
is_internal_hire_promoted | boolean | no | TRUE for the Samuel/William case: DFL hired a non-dev as a full-time dev. (column: is_internal_hire_promoted; NOT NULL default false) |
had_prior_tech_background | boolean | no | Whether the fellow already had a tech background before this placement. FALSE for internal hires who "não eram da área" (Samuel/William). (column: had_prior_tech_background; nullable) |
consent_to_report | boolean | no | LGPD consent to use this placement in external/UNICEF reports. Defaults to false (DB default) — rows exist but are excluded from external reports until consent is recorded. (column: consent_to_report; NOT NULL default false) |
evidence_url | string | no | Evidence link (LinkedIn, offer letter, contract photo, etc.). (column: evidence_url) |
notes | string | no | Free-form notes / context. (column: notes) |
created_by | string | no | auth.users.id of who logged this record (FK -> auth.users.id). Optional. (column: created_by) |
create_member_label_alias
Map a raw speaker/label string to a member by upserting into work.member_label_aliases.
create_member_label_aliasCreate Member Label Alias
Map a raw speaker/label string to a member by upserting into work.member_label_aliases. Used to resolve attendance/meeting_participants raw_label values (transcript spellings, first-name-only, etc.) back to a fellow. Idempotent: upserts on the UNIQUE (alias, member_id) constraint, so re-running the same mapping is a no-op (refreshes source/confidence/context). Provide member_id directly, or resolve a fellow name to its id with lookup_member (in the learn MCP) first. Match the alias to the EXACT raw_label string used in work.meeting_participants.
| Parameter | Type | Required | Description |
|---|---|---|---|
alias | string | yes | The raw label / speaker string to map (e.g. the exact raw_label in work.meeting_participants). (column: alias) |
member_id | string | yes | work.members.id the alias maps to (resolve a name via lookup_member in the learn MCP). FK -> work.members(id). |
source | string | no | Origin of the mapping: manual | extractor | fireflies | gmail_signature | ... DB default: 'manual'. (column: source) |
confidence | number | no | Confidence of the mapping, 0..1. DB default: 1.00. (column: confidence) |
context | string | no | Free-text traceability note, e.g. "Tainan TG msg 6515" or "attendance audit 2026-06-14 — HIGH confidence". (column: context) |
Meetings
Section titled “Meetings”Fix recurring meetings: merge duplicates, set their time window and their template.
merge_meetings
Dedup a duplicate recurring-meeting cluster by MERGING a skeleton meeting into a canonical meeting, then removing the skeleton.
merge_meetingsMerge Duplicate Recurring Meetings
Dedup a duplicate recurring-meeting cluster by MERGING a skeleton meeting into a canonical meeting, then removing the skeleton. LOSSLESS on duplicate-date collisions: it NEVER drops a transcription-bearing occurrence in favor of an empty one. For a date both meetings have, it compares transcripts on BOTH sides — if the canonical occurrence is EMPTY but the skeleton one carries a real transcript, it RE-PARENTS the skeleton occurrence onto the canonical and DELETES the empty canonical duplicate (the real transcript survives). If BOTH carry transcripts it reports a CONFLICT, keeps both, and does NOT remove the skeleton row (manual review needed). Empty skeleton duplicates of a transcript-bearing (or empty) canonical date are dropped; skeleton occurrences on a NEW date are re-parented. Other meeting_id child rows (participants/concepts/expertise/tools/underlines) are re-parented too, guarding the meeting_participants UNIQUE constraint. The skeleton meetings row is removed only when there are ZERO unresolved conflicts. Transcriptions live per-occurrence so nothing is concatenated. It also PRESERVES the work.meetings.template field: if the canonical is null/"default" and the skeleton carries a non-default template (e.g. "weekly_leaders"), the canonical is updated to the skeleton's template (carry the more-specific value forward; never downgrade). ALWAYS run with dry_run:true first on prod data to preview the change.
| Parameter | Type | Required | Description |
|---|---|---|---|
canonical_meeting_id | string | yes | work.meetings.id of the CANONICAL meeting that SURVIVES (the Fireflies-sourced row that usually holds the real transcriptions on its occurrences). |
skeleton_meeting_id | string | yes | work.meetings.id of the SKELETON meeting to merge in and then REMOVE (the empty Google-Calendar-invite row, e.g. "Updated invitation: …" or a title-variant with no cal-id). If a duplicate-date collision is resolved in the skeleton's favor (its occurrence carries the real transcript), that occurrence is re-parented onto the canonical before the skeleton is removed. |
dry_run | boolean | no | When true (recommended first), report what WOULD change without writing anything. Default: false. |
set_meeting_recurrence_time
Set the time-of-day window (recurrence_start_time / recurrence_end_time) on a single recurring meeting series in work.meetings, so its recurring occurrences render at the right hour on the /meetings time-grid.
set_meeting_recurrence_timeSet Meeting Recurrence Time Window
Set the time-of-day window (recurrence_start_time / recurrence_end_time) on a single recurring meeting series in work.meetings, so its recurring occurrences render at the right hour on the /meetings time-grid. Times are 24h wall-clock "HH:MM" or "HH:MM:SS" (Postgres time). The meeting must already be a recurring series (is_recurring = true); recurrence_day_of_week is set separately. Returns the updated row.
| Parameter | Type | Required | Description |
|---|---|---|---|
meeting_id | string | yes | work.meetings.id of the recurring series to update. |
recurrence_start_time | string | yes | Series start time-of-day, 24h "HH:MM" or "HH:MM:SS" (e.g. "12:30"). |
recurrence_end_time | string | yes | Series end time-of-day, 24h "HH:MM" or "HH:MM:SS" (e.g. "13:30"). |
set_meeting_template
Set the UI template (work.meetings.template) on a single meeting in work.meetings.
set_meeting_templateSet Meeting Template
Set the UI template (work.meetings.template) on a single meeting in work.meetings. The template selects which structured-underline UI the dfl-learn /meetings view renders — the weekly-leaders structured form is gated on template = "weekly_leaders". Allowed values: "default" | "weekly_leaders" (mirrors the dfl-learn UnderlineTemplate enum). Unknown values are rejected. Returns the updated row (id, title, template). Primary use: restore a meeting's template after a dedup-merge collapsed it to "default".
| Parameter | Type | Required | Description |
|---|---|---|---|
meeting_id | string | yes | work.meetings.id of the meeting whose template to set. |
template | enum | yes | The UI template to set. One of: "default", "weekly_leaders". One of: default, weekly_leaders. |
Meeting series data-ops
Section titled “Meeting series data-ops”Clean up recurring meeting series without losing their transcripts.
list_meeting_series
READ-ONLY discovery of recurring meeting SERIES PARENTS in work.meetings (is_recurring = true), so an operator can find duplicate/dedup targets without psql.
list_meeting_seriesList Recurring Meeting Series
READ-ONLY discovery of recurring meeting SERIES PARENTS in work.meetings (is_recurring = true), so an operator can find duplicate/dedup targets without psql. Filter by creation window (created_after / created_before), title substring, or fireflies_calendar_id substring. Each result carries the series id, title, fireflies_calendar_id, meeting_date, created_at and an occurrence accounting: occurrence_count, occurrences_with_transcript (real transcript TEXT) and occurrences_with_transcript_id (a Fireflies transcript id). Set include_occurrences:true to also get the per-occurrence rows (id, occurrence_date, status, has_transcript, fireflies_transcript_id). Transcript TEXT is NEVER returned — it is tens/hundreds of KB per occurrence. Results are sorted by created_at ascending. Use this before remove_meeting_series to confirm exactly which series you are about to remove and how much real content hangs off it.
| Parameter | Type | Required | Description |
|---|---|---|---|
created_after | string | no | Only series created at or after this ISO-8601 timestamp (e.g. "2026-07-28T00:00:00Z"). The canonical way to answer "which series were created today?". |
created_before | string | no | Only series created at or before this ISO-8601 timestamp. |
title_contains | string | no | Case-insensitive substring match on work.meetings.title. |
fireflies_calendar_id_contains | string | no | Case-insensitive substring match on fireflies_calendar_id. Useful to group the "_R<instance>" forks Google mints for the same base calendar id (e.g. pass the base id without the suffix). |
include_occurrences | boolean | no | When true, include the per-occurrence rows for each series (id, occurrence_date, status, has_transcript, fireflies_transcript_id). Never includes transcript text. Default: false. |
limit | number | no | Maximum number of series to return. Default: 100. |
remove_meeting_series
Remove ONE recurring meeting SERIES PARENT from work.meetings, promoting its occurrences to standalone ("avulsa") meetings FIRST so the ON DELETE CASCADE can never destroy a transcript.
remove_meeting_seriesRemove Recurring Meeting Series (data-ops)
Remove ONE recurring meeting SERIES PARENT from work.meetings, promoting its occurrences to standalone ("avulsa") meetings FIRST so the ON DELETE CASCADE can never destroy a transcript. Transcripts live PER-OCCURRENCE and all 7 FKs onto work.meetings(id) are ON DELETE CASCADE, so the order promote -> verify -> delete IS the safety mechanism (an MCP tool cannot hold a Postgres transaction across calls). Guards, all of which ABORT rather than guess: (a) UNTRACKABLE — any occurrence with transcript text but no fireflies_transcript_id; (b) ALREADY-PROMOTED — occurrences whose transcript id already exists on a meetings row are skipped, not re-inserted (partial UNIQUE uq_meetings_fireflies_transcript_id), which also makes the tool idempotent on retry; (c) CASCADE — refuses when meeting_participants / meeting_concepts / meeting_expertise / meeting_tools / meeting_transcript_segments / meeting_underlines have rows on this parent or its occurrences (use merge_meetings instead, it knows how to re-parent those); (d) a post-promotion ASSERT that every surviving occurrence has a standalone counterpart before anything is deleted. Promoted rows get is_recurring:false and fireflies_calendar_id:null (deliberately detached from any series), the parent's title, the occurrence's content, and a meeting_date rebuilt from the occurrence date plus the parent's time-of-day. SIDE EFFECT: work.meetings has an AFTER INSERT trigger (meetings_to_n8n) that POSTs each new row to the n8n process_devfellowship_meetings webhook; a user-JWT tool cannot disable it, so every promotion fires one n8n workflow — the count is reported as n8n_webhook_inserts. ALWAYS run with dry_run:true first on prod: it returns the exact same result shape with nothing written.
| Parameter | Type | Required | Description |
|---|---|---|---|
series_meeting_id | string | yes | work.meetings.id of the recurring SERIES PARENT to remove. Find it with list_meeting_series. |
occurrence_policy | enum | no | How to treat the series occurrences. "promote_real_discard_empty" (DEFAULT): occurrences carrying transcript TEXT or a fireflies_transcript_id are PROMOTED to standalone meetings; occurrences with neither (empty materializer placeholders, typically future-dated) are DELETED — recreating those would only manufacture junk rows. "promote_all": every occurrence is promoted, even the empty placeholders. "require_empty": pure safety mode — refuse to do anything unless the series has ZERO occurrences. One of: promote_real_discard_empty, promote_all, require_empty. |
allow_non_recurring | boolean | no | By default the tool REFUSES a meeting with is_recurring = false, so it can never be pointed at a standalone ("avulsa") meeting by mistake. Set true to override deliberately. Default: false. |
dry_run | boolean | no | When true, NOTHING is written — returns the exact same result shape describing what WOULD happen (including n8n_webhook_inserts). ALWAYS run dry_run:true first on prod. Default: false. |
promote_occurrence_to_standalone
Promote ONE work.meeting_occurrences row to a standalone ("avulsa") work.meetings row and then detach/delete the occurrence.
promote_occurrence_to_standalonePromote Meeting Occurrence to Standalone Meeting
Promote ONE work.meeting_occurrences row to a standalone ("avulsa") work.meetings row and then detach/delete the occurrence. The generic primitive underneath remove_meeting_series; use it to pull a single meeting out of a recurring series. The new meeting gets is_recurring:false, fireflies_calendar_id:null (deliberately detached), the parent series' title (or title_override), the occurrence's content, and a meeting_date rebuilt from the occurrence date plus the parent's time-of-day. Guards: it refuses an occurrence carrying transcript text with NO fireflies_transcript_id unless keep_occurrence:true (the copy could not be verified by transcript id, so deleting the source would be unsound); it never mints a second meetings row for a transcript id that already exists (partial UNIQUE uq_meetings_fireflies_transcript_id) and reports the existing meeting instead, which makes it idempotent; and it refuses when meeting_participants / meeting_underlines rows FK on this occurrence, since those are ON DELETE CASCADE and would be destroyed. Order is the safety mechanism: INSERT and confirm, then delete. SIDE EFFECT: the meetings_to_n8n AFTER INSERT trigger POSTs each new row to the n8n process_devfellowship_meetings webhook and a user-JWT tool cannot disable it, so one n8n workflow fires per promotion (reported as n8n_webhook_inserts). Run dry_run:true first on prod.
| Parameter | Type | Required | Description |
|---|---|---|---|
occurrence_id | string | yes | work.meeting_occurrences.id to promote. Find it with list_meeting_series({include_occurrences:true}). |
title_override | string | no | Title for the new standalone meeting. Defaults to the parent series' title (occurrences have no title of their own). |
keep_occurrence | boolean | no | When true, create the standalone meeting but LEAVE the occurrence in place — a copy-then-review flow where nothing is destroyed. Also the escape hatch for an occurrence whose transcript has no fireflies_transcript_id, or one that still has participant/underline rows attached. Default: false (the occurrence is deleted after the copy is confirmed). |
dry_run | boolean | no | When true, NOTHING is written — returns the same result shape describing what WOULD happen. ALWAYS run dry_run:true first on prod. Default: false. |
Weekly updates
Section titled “Weekly updates”Read-only. What one member did in the last N days, per source, with a daily text ready to paste. Mirrors the Weekly updates page of dfl-learn (/weekly-updates).
What one person did in the last days, ready to paste as a daily update.
get_weekly_updates
Read-only. What one member did in the last N days (Brasília calendar days, today included): tasks moved to done/dev_completed, tasks in progress, merged PRs (review_request.review_requests), Core Daily lines they said…
get_weekly_updatesGet weekly updates for a member
Read-only. What one member did in the last N days (Brasília calendar days, today included): tasks moved to done/dev_completed, tasks in progress, merged PRs (review_request.review_requests), Core Daily lines they said (resolved per meeting: segment attribution, then the top alias, then the exact name; any ambiguity leaves the line out), and plans whose status changed (plans app, with your session). Each source degrades on its own: see unavailable and truncated. daily_text is ready to paste in the "Bom dia / Ontem / Hoje / No blockers" format, one line per item, with no PR numbers, repos, task identifiers or links. Defaults to you when member_id is omitted.
| Parameter | Type | Required | Description |
|---|---|---|---|
member_id | string | no | public.members id; defaults to the caller. |
days | number | no | Window in Brasília calendar days, 1–14. Default: 7. |