Skip to content

work — full tool reference

Projects, epics, deliveries, tasks, member placement.

Endpointhttps://work.mcp.devfellowship.com/mcp
Packagepackages/dfl-mcp-work
Tools34
ToolDescription
list_projectsList all projects with optional filters.
get_projectGet a specific project by ID with its epics.
create_projectCreate a new project.
update_projectUpdate an existing project.
delete_projectDelete a project. This will fail if the project has associated epics.
list_epicsList all epics with optional filters.
get_epicGet a specific epic by ID with its deliveries and tasks.
create_epicCreate 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.
update_epicUpdate an existing epic.
delete_epicDelete an epic. This will fail if the epic has associated deliveries or tasks.
list_deliveriesList all deliveries with optional filters.
get_deliveryGet a specific delivery by ID with its epic and tasks.
create_deliveryCreate a new delivery.
update_deliveryUpdate an existing delivery.
delete_deliveryDelete a delivery.
list_tasksList all tasks with optional filters.
get_taskGet a specific task by ID with its epic and delivery.
create_taskCreate 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.
update_taskUpdate an existing task.
delete_taskDelete a task.
create_placementRecord 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.
create_member_label_aliasMap 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.
list_business_unitsList all business units with optional filters. Requires finance/admin/owner role.
get_business_unitGet a specific business unit by ID. Requires finance/admin/owner role.
create_business_unitCreate a new business unit. Requires finance/admin/owner role.
update_business_unitUpdate an existing business unit. Requires finance/admin/owner role.
delete_business_unitDelete a business unit by ID. Requires finance/admin/owner role.
merge_meetingsDedup 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.
set_meeting_recurrence_timeSet 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.
set_meeting_templateSet 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”.
list_meeting_seriesREAD-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.
remove_meeting_seriesRemove 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.
promote_occurrence_to_standalonePromote 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.
get_weekly_updatesRead-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.

List Projects

List all projects with optional filters.

ParameterTypeRequiredDescription
limitnumbernoMaximum number of projects to return (default: 50, max: 100)
offsetnumbernoNumber of projects to skip (for pagination)
business_unit_idstringnoFilter by business unit ID
searchstringnoSearch by project name

Get Project

Get a specific project by ID with its epics.

ParameterTypeRequiredDescription
idstringyesThe UUID of the project

Create Project

Create a new project.

ParameterTypeRequiredDescription
namestringyesProject name
business_unit_idstringnoBusiness unit ID

Update Project

Update an existing project.

ParameterTypeRequiredDescription
idstringyesThe UUID of the project to update
namestringnoProject name
business_unit_idstringnoBusiness unit ID

Delete Project

Delete a project. This will fail if the project has associated epics.

ParameterTypeRequiredDescription
idstringyesThe UUID of the project to delete

List Epics

List all epics with optional filters.

ParameterTypeRequiredDescription
limitnumbernoMaximum number of epics to return (default: 50, max: 100)
offsetnumbernoNumber of epics to skip (for pagination)
project_idstringnoFilter by project ID
statusenumnoFilter by status One of: pending, in_progress, done, no_longer_needed.
searchstringnoSearch by epic name

Get Epic

Get a specific epic by ID with its deliveries and tasks.

ParameterTypeRequiredDescription
idstringyesThe 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. 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.

ParameterTypeRequiredDescription
namestringyesEpic name
project_idstringnoProject ID
notesstringnoEpic notes/description
statusenumnoEpic status One of: pending, in_progress, done, no_longer_needed.
is_long_livedbooleannoWhether this is a long-lived epic

Update Epic

Update an existing epic.

ParameterTypeRequiredDescription
idstringyesThe UUID of the epic to update
namestringnoEpic name
project_idstringnoProject ID
notesstringnoEpic notes/description
statusenumnoEpic status One of: pending, in_progress, done, no_longer_needed.
is_long_livedbooleannoWhether this is a long-lived epic

Delete Epic

Delete an epic. This will fail if the epic has associated deliveries or tasks.

ParameterTypeRequiredDescription
idstringyesThe UUID of the epic to delete

List Deliveries

List all deliveries with optional filters.

ParameterTypeRequiredDescription
limitnumbernoMaximum number of results (default 50)
offsetnumbernoNumber of results to skip
epic_idstringnoFilter by epic ID
owner_idstringnoFilter by owner (member) ID
statusenumnoFilter by status One of: pending, in_progress, completed, canceled, no_longer_needed.
searchstringnoSearch in name and notes

Get Delivery

Get a specific delivery by ID with its epic and tasks.

ParameterTypeRequiredDescription
idstringyesThe UUID of the delivery

Create Delivery

Create a new delivery.

ParameterTypeRequiredDescription
namestringyesDelivery name
notesstringnoDelivery notes
epic_idstringyesEpic ID (required)
owner_idstringnoOwner (member) ID
statusenumnoDelivery status One of: pending, in_progress, completed, canceled, no_longer_needed.
pricenumbernoDelivery price
price_per_pointnumbernoPrice per story point
total_pointsnumbernoTotal story points
transaction_idstringnoTransaction ID

Update Delivery

Update an existing delivery.

ParameterTypeRequiredDescription
idstringyesThe UUID of the delivery to update
namestringnoDelivery name
notesstringnoDelivery notes
epic_idstringnoEpic ID
owner_idstringnoOwner (member) ID
statusenumnoDelivery status One of: pending, in_progress, completed, canceled, no_longer_needed.
pricenumbernoDelivery price
price_per_pointnumbernoPrice per story point
total_pointsnumbernoTotal story points
number_of_tasksnumbernoNumber of tasks
number_of_completed_tasksnumbernoNumber of completed tasks
transaction_idstringnoTransaction ID

Delete Delivery

Delete a delivery.

ParameterTypeRequiredDescription
idstringyesThe UUID of the delivery to delete

List Tasks

List all tasks with optional filters.

ParameterTypeRequiredDescription
limitnumbernoMaximum number of tasks to return (default: 50, max: 100)
offsetnumbernoNumber of tasks to skip (for pagination)
epic_idstringnoFilter by epic ID
delivery_idstringnoFilter by delivery ID
owner_idstringnoFilter by owner (member) ID
statusenumnoFilter by status One of: to_do, in_progress, dev_completed, done, no_longer_needed, blocked.
searchstringnoSearch by task name

Get Task

Get a specific task by ID with its epic and delivery.

ParameterTypeRequiredDescription
idstringyesThe 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. 🔴 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.

ParameterTypeRequiredDescription
namestringyesTask name
descriptionstringnoTask description
epic_idstringnoEpic ID
delivery_idstringnoDelivery ID
owner_idstringnoOwner (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.
statusenumnoTask status One of: to_do, in_progress, dev_completed, done, no_longer_needed, blocked.
pointsnumbernoStory points
prioritynumbernoPriority (lower = higher priority)
estimated_datestringnoEstimated completion date (ISO format)
attachmentsstring[]noArray of attachment URLs
acceptance_criteriastring[]noWhat 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_idenumyesWhat 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.
contextobjectyesContext package: why the task exists, what was done, and the links
actor_slugstringnoThe 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.

Update Task

Update an existing task.

ParameterTypeRequiredDescription
idstringyesThe UUID of the task to update
namestringnoTask name
descriptionstringnoTask description
epic_idstringnoEpic ID
delivery_idstringnoDelivery ID
owner_idstringnoOwner (member) ID
statusenumnoTask status One of: to_do, in_progress, dev_completed, done, no_longer_needed, blocked.
pointsnumbernoStory points
prioritynumbernoPriority (lower = higher priority)
estimated_datestringnoEstimated completion date (ISO format)
attachmentsstring[]noArray of attachment URLs
acceptance_criteriastring[]noWhat 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_idenumnoStage ID (a task without one is invisible on the kanban board) One of: spec, design, execution, review, decision, qa.

Delete Task

Delete a task.

ParameterTypeRequiredDescription
idstringyesThe UUID of the task to delete

Create 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.

ParameterTypeRequiredDescription
member_idstringyespublic.members.id of the fellow (resolve a name via lookup_member in the learn MCP). FK -> public.members(id).
companystringyesEmployer / company / org name (column: company).
rolestringyesRole / job title (column: role).
typeenumnoKind of placement. DB default: employed. (column: type / work.placement_type) One of: employed, freelance, founded, internship.
started_atstringyesStart date (ISO date, e.g. 2025-03-01). (column: started_at)
ended_atstringnoEnd date (ISO date); omit if still active. (column: ended_at)
placement_tierenumnoA = 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_recordenumnoConfidence 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_helpedenum[]noHow 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_promotedbooleannoTRUE 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_backgroundbooleannoWhether 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_reportbooleannoLGPD 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_urlstringnoEvidence link (LinkedIn, offer letter, contract photo, etc.). (column: evidence_url)
notesstringnoFree-form notes / context. (column: notes)
created_bystringnoauth.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. 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.

ParameterTypeRequiredDescription
aliasstringyesThe raw label / speaker string to map (e.g. the exact raw_label in work.meeting_participants). (column: alias)
member_idstringyeswork.members.id the alias maps to (resolve a name via lookup_member in the learn MCP). FK -> work.members(id).
sourcestringnoOrigin of the mapping: manual | extractor | fireflies | gmail_signature | … DB default: ‘manual’. (column: source)
confidencenumbernoConfidence of the mapping, 0..1. DB default: 1.00. (column: confidence)
contextstringnoFree-text traceability note, e.g. “Tainan TG msg 6515” or “attendance audit 2026-06-14 — HIGH confidence”. (column: context)

List Business Units

List all business units with optional filters. Requires finance/admin/owner role.

ParameterTypeRequiredDescription
limitnumbernoMaximum number of business units to return (default: 50, max: 100)
offsetnumbernoNumber of business units to skip (for pagination)
searchstringnoSearch by business unit name
tagstringnoFilter by tag

Get Business Unit

Get a specific business unit by ID. Requires finance/admin/owner role.

ParameterTypeRequiredDescription
idstringyesBusiness unit ID (UUID)

Create Business Unit

Create a new business unit. Requires finance/admin/owner role.

ParameterTypeRequiredDescription
namestringyesBusiness unit name
profile_image_urlstringnoProfile image URL
tagsstring[]noArray of tags

Update Business Unit

Update an existing business unit. Requires finance/admin/owner role.

ParameterTypeRequiredDescription
idstringyesBusiness unit ID (UUID)
namestringnoBusiness unit name
profile_image_urlstringnoProfile image URL, null to remove
tagsstring[]noArray of tags, null to remove

Delete Business Unit

Delete a business unit by ID. Requires finance/admin/owner role.

ParameterTypeRequiredDescription
idstringyesBusiness unit ID (UUID)

Merge 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.

ParameterTypeRequiredDescription
canonical_meeting_idstringyeswork.meetings.id of the CANONICAL meeting that SURVIVES (the Fireflies-sourced row that usually holds the real transcriptions on its occurrences).
skeleton_meeting_idstringyeswork.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_runbooleannoWhen true (recommended first), report what WOULD change without writing anything. Default: false.

Set 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.

ParameterTypeRequiredDescription
meeting_idstringyeswork.meetings.id of the recurring series to update.
recurrence_start_timestringyesSeries start time-of-day, 24h “HH:MM” or “HH:MM:SS” (e.g. “12:30”).
recurrence_end_timestringyesSeries 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. 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”.

ParameterTypeRequiredDescription
meeting_idstringyeswork.meetings.id of the meeting whose template to set.
templateenumyesThe UI template to set. One of: “default”, “weekly_leaders”. One of: default, weekly_leaders.

List 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.

ParameterTypeRequiredDescription
created_afterstringnoOnly 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_beforestringnoOnly series created at or before this ISO-8601 timestamp.
title_containsstringnoCase-insensitive substring match on work.meetings.title.
fireflies_calendar_id_containsstringnoCase-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_occurrencesbooleannoWhen true, include the per-occurrence rows for each series (id, occurrence_date, status, has_transcript, fireflies_transcript_id). Never includes transcript text. Default: false.
limitnumbernoMaximum number of series to return. Default: 100.

Remove 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.

ParameterTypeRequiredDescription
series_meeting_idstringyeswork.meetings.id of the recurring SERIES PARENT to remove. Find it with list_meeting_series.
occurrence_policyenumnoHow 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_recurringbooleannoBy 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_runbooleannoWhen 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 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.

ParameterTypeRequiredDescription
occurrence_idstringyeswork.meeting_occurrences.id to promote. Find it with list_meeting_series({include_occurrences:true}).
title_overridestringnoTitle for the new standalone meeting. Defaults to the parent series’ title (occurrences have no title of their own).
keep_occurrencebooleannoWhen 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_runbooleannoWhen true, NOTHING is written — returns the same result shape describing what WOULD happen. ALWAYS run dry_run:true first on prod. Default: false.

Get 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.

ParameterTypeRequiredDescription
member_idstringnopublic.members id; defaults to the caller
daysnumbernoWindow in Brasília calendar days, 1–14 Default: 7.