# Meeting Workflow Engine Meeting Assistant has a small local workflow engine for meeting-specific automation. It is intended for rules that are too personal or environment-specific to hard-code, such as adding default attendees, cleaning meeting titles, binding projects, or adding context notes when known speakers are detected. The workflow engine is deliberately narrow. It runs from a local YAML file, evaluates rules against the current meeting note read from disk, and applies a small set of meeting-safe mutations. ## Configuration The rules file path is configured through `MeetingAssistant:Automation:RulesPath`. ```json { "MeetingAssistant": { "Automation": { "RulesPath": "meeting-rules.local.yaml" } } } ``` `meeting-rules.local.yaml` is the default local rules file and is ignored by git. The path may be absolute, relative to the process working directory, or use environment variables. If the configured path is empty, missing, or points to a blank file, the workflow engine does nothing. The tray menu includes `Open agent`, which opens the `Meeting Summary Agent` chat window for this configured rules file, the local speaker identity database, appsettings configuration, and application logs. The assistant uses the summarizer agent configuration by default and can be overridden through `MeetingAssistant:WorkflowRulesEditor`. ```json { "MeetingAssistant": { "WorkflowRulesEditor": { "Endpoint": "", "KeyEnv": "", "Model": "", "UseStreaming": null, "EnableThinking": null, "ReasoningEffort": null, "MaxOutputTokens": null, "EnableCompaction": null, "CompactionRemainingRatio": null, "ResponsesCompactPath": "", "InitialPrompt": "" } } } ``` Blank editor values inherit from `MeetingAssistant:Agent`. This keeps the editor on the same LiteLLM Responses endpoint and model as the summarizer unless a value is explicitly configured. Each user-submitted editor turn sends its first model request with `X-Initiator: user`; follow-up model requests in the same turn, such as tool-call continuations, use `X-Initiator: agent`. While the agent is working, the window shows visible reasoning summaries returned by the Responses endpoint and called tool names as lightweight activity lines above the bottom activity line. After the turn completes, those lines move into a collapsible `Worked for ` expander between the user and assistant messages. The editor agent prompt includes this document and is restricted to these tools: - `read_rules`: read the configured rules file, optionally by inclusive 1-based line range. - `write_rules`: append to the configured rules file by default after validating that the complete YAML document parses as a supported workflow rules document. Pass `replace_file: true` only when intentionally replacing the whole rules file. When validation fails, the tool refuses the write and returns the invalid rule/field plus supported values so the agent can fix and retry. - `search`: search only the configured rules file with ripgrep-style syntax. `POST /diagnostics/workflow/reload` reloads application configuration without restarting the process. Use it after changing workflow automation configuration such as `MeetingAssistant:Automation:RulesPath`. The endpoint returns the currently bound rules path: ```json { "rulesPath": "meeting-rules.local.yaml" } ``` The YAML rules file itself is read for every workflow event, so changing the contents of the same rules file does not require this endpoint. Use the endpoint when the application configuration changes, for example when pointing `RulesPath` to a different YAML file. A meeting run that already captured its options may continue with those options; future runs and future configuration reads use the reloaded configuration. ## Execution Model The engine runs when `MeetingRecordingCoordinator` emits a meeting workflow event: - `created`: after the meeting note and assistant context artifact are created. - `state_transition`: after the assistant context state moves forward. - `speaker_identified`: when live or final speaker identification reports a display name. - `transcript_line`: after a formatted live transcript line is durably appended, before any changed line is rewritten in place, and before lines are used in full transcript rewrites. - `attendee_added`: before a newly added attendee is stored in the meeting note. `attendee_added` is emitted only by Meeting Assistant code paths that intentionally add an attendee through the workflow engine, notification actions, OCR/screenshot processing, summarizer tools, or other attendee-add operations. Direct edits to a meeting note file, including direct artifact/frontmatter repair writes by an agent, are left verbatim and do not trigger attendee automation. When a recording is started by accepting a calendar notification, the cached appointment metadata is applied before the `created` event. Rules still receive the normal `created` event and the normal `collecting metadata` to `transcribing` state transition, but they see the accepted appointment title, attendees, agenda, and scheduled end instead of a generated placeholder or a separate Outlook current-meeting lookup result. For every event, the engine: 1. Loads the current rules file. 2. Reads the latest meeting note from disk. 3. Evaluates every matching rule in file order. 4. Applies each matching rule's steps in order. 5. Rebuilds the template model after every step, so later steps can see earlier mutations. 6. Saves the meeting note once if any meeting-note step changed it. 7. Updates assistant-context frontmatter links/title from the saved meeting note after note changes. For `transcript_line` events, the engine returns the possibly updated transcript line to the caller. Live transcript appends first write the original formatted line and keep a reference to that exact body line, then run transcript-line rules out of band. Later transcript segments can continue to be appended while the workflow runs. If rules return a changed line, Meeting Assistant rewrites the referenced line in the transcript file. If a transcript-line rule fails during live recording, Meeting Assistant logs the failure and keeps the original line so later transcript segments can continue to be written. Rules are best-effort automation for live transcript persistence. The settings/logs write tool rejects invalid YAML, unknown steps, unsupported set-property fields, trigger or condition entries without a supported key, and step value Razor templates that fail against the workflow template model before writing the rules file. Invalid NCalc expressions can still fail at workflow runtime. Workflow execution logs every triggered rule with its rule name and event type, logs completion with whether the note or transcript line changed, and logs rule failures with the rule name and event type. ## YAML Shape The top-level YAML document contains a `rules` array. ```yaml rules: - name: add-default-attendee on: - created: {} if: - condition: meeting.attendees.count = 0 steps: - uses: add_attendee value: Manuel ``` Each rule has: - `name`: log-friendly rule name. - `on`: one or more triggers. At least one must match. - `if`: optional list of conditions. All top-level conditions must pass. - `steps`: ordered actions to run when the rule matches. ## Triggers ### `created` Runs after a meeting note is created. ```yaml on: - created: {} ``` ### `state_transition` Runs when the assistant context state transitions. `from` and `to` are optional filters; omitted filters match any value. ```yaml on: - state_transition: from: collecting metadata to: transcribing ``` Supported state names are: - `collecting metadata` - `transcribing` - `speaker recognition` - `summarizing` - `finished` - `error` ### `speaker_identified` Runs when speaker identification reports a display name. `name` is optional; when supplied, it matches case-insensitively. ```yaml on: - speaker_identified: name: Ada ``` ### `transcript_line` Runs before a formatted transcript line is written. `speaker` is optional; when supplied, it matches the effective speaker label case-insensitively. ```yaml on: - transcript_line: speaker: Guest-1 ``` ### `attendee_added` Runs before a newly added attendee is stored. `equals`, `contains`, and `regex` filters are optional; omitted filters match any attendee. `equals` and `contains` are case-insensitive. ```yaml on: - attendee_added: contains: Contoso ``` ```yaml on: - attendee_added: regex: '@contoso\.com>$' ``` ## Conditions Conditions use NCalc expressions. Dotted workflow variables may be written directly; the engine rewrites them into NCalc parameters internally. ```yaml if: - condition: meeting.attendees.count = 0 ``` Available condition variables: - `meeting.attendees.count` - `meeting.attendees` - `meeting.projects` - `meeting.title` - `meeting.state` - `event.type` - `state.from` - `state.to` - `speaker.name` - `transcript.line` - `transcript.speaker` - `attendee.name` Available helper functions: - `contains(haystack, needle)`: case-insensitive string or collection contains. - `starts_with(value, prefix)`: case-insensitive string prefix check. - `ends_with(value, suffix)`: case-insensitive string suffix check. Nested boolean groups are supported: ```yaml if: - and: - condition: contains(meeting.title, '[External]') - not: condition: contains(meeting.projects, 'Internal') ``` Top-level `if` entries are combined with `and`. ## Step Templating Step `value` fields can be plain strings or Razor templates. Razor templates receive this model: - `Model.Meeting.Title` - `Model.Meeting.Attendees` - `Model.Meeting.Projects` - `Model.Meeting.State` - `Model.Event.Type` - `Model.Event.From` - `Model.Event.To` - `Model.Speaker.Name` - `Model.Transcript.Line` - `Model.Transcript.Speaker` - `Model.Attendee.Name` Example: ```yaml steps: - uses: add_context value: "Known speaker identified: @Model.Speaker.Name" ``` The engine invokes Razor when the value contains an unescaped `@` that is not part of a valid email address token. Literal email addresses such as `Support@contoso.com` are left unchanged; if the same value also contains a Razor expression, the email `@` is escaped before rendering so the address still appears normally in the rendered result. Rendered values are plain text for markdown artifacts, not HTML. UTF-8 characters such as `ß`, `ö`, and `ä` are preserved after Razor rendering instead of being persisted as HTML entities. ## Steps ### `add_attendee` Adds a normalized attendee display name to meeting note frontmatter if it is not already present case-insensitively. ```yaml steps: - uses: add_attendee value: Manuel ``` ### `remove_attendee` Removes attendees whose normalized display name matches the rendered value. ```yaml steps: - uses: remove_attendee value: "Guest" ``` ### `set_property` Sets a supported property. Supported meeting properties are `title` and `meeting.title`. During `transcript_line` events, `transcript.line` is also supported. During `attendee_added` events, `attendee.name` is supported. For live transcription, changing `transcript.line` rewrites the referenced formatted line after the workflow task completes. For attendee additions, changing `attendee.name` stores the transformed attendee instead of the original added value. Rules triggered by `attendee_added` are transformation-only: their steps must use `set_property` with `property: attendee.name`. They can still use trigger filters, conditions, and Razor templates to decide and compute the transformed value, but they cannot perform note/context side effects. ```yaml steps: - uses: set_property property: title value: "@Model.Meeting.Title.Replace('[External] ', '')" ``` ```yaml steps: - uses: set_property property: transcript.line value: '@Model.Transcript.Line.Replace("*****", "[redacted]")' ``` ```yaml steps: - uses: set_property property: attendee.name value: '@Model.Attendee.Name.Replace(" (Contoso)", "")' ``` ### `add_context` Appends rendered text to the assistant context artifact body. This step does not count as a meeting-note mutation and does not cause a meeting-note save by itself. ```yaml steps: - uses: add_context value: "Ada was identified; check project ownership notes." ``` ### `add_project` Adds a project name to meeting note frontmatter if it is not already present case-insensitively. ```yaml steps: - uses: add_project value: Meeting Assistant ``` ## Examples Add a default attendee only for empty meetings: ```yaml rules: - name: add-me on: - created: {} if: - condition: meeting.attendees.count = 0 steps: - uses: add_attendee value: Manuel ``` Remove a title marker after metadata collection: ```yaml rules: - name: clean-external-marker on: - state_transition: from: collecting metadata if: - condition: starts_with(meeting.title, '[External] ') steps: - uses: set_property property: title value: "@Model.Meeting.Title.Replace(\"[External] \", \"\")" ``` Add context when a speaker is identified: ```yaml rules: - name: note-ada on: - speaker_identified: name: Ada steps: - uses: add_context value: "Ada was identified. Check whether the architecture decision log needs an update." ``` Add context when metadata gathering ends with a specific attendee: ```yaml rules: - name: note-review-owner on: - state_transition: from: collecting metadata if: - condition: contains(meeting.attendees, 'Manuel') steps: - uses: add_context value: "Manuel is attending; include implementation follow-ups in the summary." ``` Replace Azure Speech masked profanity before transcript markdown is written: ```yaml rules: - name: redact-masked-profanity on: - transcript_line: {} if: - condition: contains(transcript.line, '*****') steps: - uses: set_property property: transcript.line value: '@Model.Transcript.Line.Replace("*****", "[redacted]")' ``` Clean attendee names as they are added: ```yaml rules: - name: clean-contoso-attendees on: - attendee_added: contains: Contoso steps: - uses: set_property property: attendee.name value: '@Model.Attendee.Name.Replace(" (Contoso)", "")' ``` ## Implementation Notes Core files: - `MeetingAssistant/Workflow/MeetingWorkflowEngine.cs` - `MeetingAssistant/Workflow/MeetingWorkflowModels.cs` - `MeetingAssistant/Workflow/MeetingWorkflowEvent.cs` - `MeetingAssistant/Workflow/FileMeetingWorkflowRulesProvider.cs` - `MeetingAssistant/Workflow/WorkflowRulesEditorChatPipeline.cs` - `MeetingAssistant/Workflow/WorkflowRulesEditorTools.cs` - `MeetingAssistant/Workflow/MewUiWorkflowRulesEditorWindowService.Windows.cs` - `MeetingAssistant/Recording/MeetingRecordingCoordinator.cs` - `MeetingAssistant.Tests/MeetingWorkflowEngineTests.cs` - `MeetingAssistant.Tests/MeetingWorkflowDiagnosticEndpointTests.cs` - `MeetingAssistant.Tests/WorkflowRulesEditorTests.cs` When extending the workflow engine: 1. Update OpenSpec requirements if behavior changes. 2. Add behavior tests through `MeetingWorkflowEngine` or the recording coordinator event surface. 3. Keep the supported trigger, condition, template model, and step lists in this document current. 4. Keep `README.md` as the short operational overview and link back here for details. 5. Keep the local rules file ignored by git; do not commit personal automation rules.