Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
20 KiB
PRD — CRM Workflow Follow-up Actions
- Slug:
workflow-crm-followup-actions - Date:
2026-04-25 - Status: Draft
Summary
Add the remaining CRM workflow actions after the first-pass CRM plan (workflow-crm-actions) lands. This follow-up plan covers interaction taxonomy, simple activity status transitions, quote creation/search/conversion/approval, and CRM activity tagging.
Planned actions:
crm.create_interaction_typecrm.update_activity_statuscrm.create_quotecrm.add_quote_itemcrm.create_quote_from_templatecrm.find_quotescrm.submit_quote_for_approvalcrm.convert_quotecrm.tag_activity
Explicitly out of scope: crm.create_client_note. Use clients.add_note for client notes and add a future contacts.add_note action under the Contact module if contact note automation is needed.
This plan intentionally builds on discoveries from the recent workflow updates and the first CRM plan: shared runtime actions must not import withAuth server action wrappers directly, picker metadata should follow current Workflow Designer conventions, event publication should use lazy shared-runtime-safe helpers, and DB-backed tests should validate real migrated schema behavior.
Problem
The first-pass CRM plan unlocks activity lookup/update/scheduling and quote sending, but it leaves several high-value CRM automation gaps:
- MSPs cannot create their own CRM activity taxonomy from workflows.
- Workflows need a simple “mark this activity completed/closed/won” action without constructing a full patch object.
- Quote automation remains incomplete without create/search/item/approval/conversion actions.
- CRM activity tagging is not available for interactions/activities.
These gaps matter because MSP automations often span support, account management, and billing: identify a client state, tag/account-classify it, create or submit a quote, and convert accepted quote value along the way.
Goals
- Complete the next CRM workflow action layer after first-pass activity and send-quote support.
- Keep action IDs under
crm.*so the existing Designer CRM group continues to work. - Provide quote pipeline actions that are safe, permission-checked, and consistent with existing quote lifecycle rules.
- Provide CRM taxonomy and status helpers that simplify common workflow authoring.
- Provide a dedicated tag action for CRM activities/interactions.
- Keep note creation on module-specific actions: existing
clients.add_noteand a futurecontacts.add_noteif needed. - Reuse existing model/service behavior where safe, extracting shared-safe helpers where necessary.
Non-goals
- Replacing first-pass CRM actions (
crm.find_activities,crm.update_activity,crm.schedule_activity,crm.send_quote). - Replacing Client module workflow actions such as
clients.add_noteorclients.add_interaction. - Adding a CRM-scoped client/contact note wrapper; note automation should use module-specific actions (
clients.add_note, futurecontacts.add_note). - Replacing quote UI/API behavior or adding new quote lifecycle states.
- Introducing a generic arbitrary-table patch action.
- Calling
withAuthserver actions directly fromshared/workflow/runtime. - Building new Workflow Designer controls beyond metadata-driven picker support.
- Adding broad quote item editing beyond the create-quote input scope described here.
Prerequisites / Dependencies
- First-pass CRM plan should be implemented or at least its shared helper decisions should be resolved:
- lazy workflow event publisher helper for CRM actions
- CRM permission mapping
- quote shared-helper/package-boundary approach
- common CRM activity summary schemas
- Recent Client workflow action patterns remain the model for picker metadata, event publication, idempotency, and DB-backed tests.
- Quote actions must respect existing quote status transitions from
packages/billing/src/schemas/quoteSchemas.ts.
Users and Primary Flows
Users
- MSP admin building cross-functional workflows.
- Account manager automating CRM follow-ups and QBR/upsell workflows.
- Sales/finance operator automating quote pipeline steps.
- Internal Alga PSA engineer maintaining Workflow Runtime V2 business operations.
Primary flows
-
Create CRM taxonomy automatically
- Workflow detects a new MSP process or onboarding template.
- Workflow creates a custom interaction type such as
QBR,Site Visit, orUpsell Callif it does not exist.
-
Mark an activity status without a full patch
- Workflow finds or creates an activity.
- Workflow runs
crm.update_activity_statuswith a target status ID or status name. - The action validates the interaction status and returns the previous/current status.
-
Create and build a quote
- Workflow creates a quote header for a client/contact after a qualifying ticket or opportunity event.
- Workflow either creates a blank quote header with
crm.create_quoteand adds items withcrm.add_quote_item, or creates a populated quote from a template withcrm.create_quote_from_template. - Workflow optionally submits the quote for approval when tenant settings or business rules require it.
-
Find quotes before branching
- Workflow checks whether the client already has open quotes.
- If none exist, it creates one; if one exists, it updates/sends/submits it through existing CRM quote actions.
-
Convert an accepted quote
- Workflow receives a quote accepted event or polls/fetches accepted quote state.
- Workflow converts quote content to a draft contract, draft invoice, or both based on selected items.
-
Tag CRM activities
- Workflow applies tags like
Needs QBR,Upsell Candidate, orOnboarding Riskto interactions/activities. - Client and contact tagging stay in their own module actions.
- Tag definition creation and tag application events are emitted consistently.
- Workflow applies tags like
UX / UI Notes
- All actions should appear under the existing Workflow Designer CRM group via the
crm.*prefix. - Suggested labels:
crm.create_interaction_type→ Create Activity Typecrm.update_activity_status→ Update Activity Statuscrm.create_quote→ Create Quotecrm.add_quote_item→ Add Quote Itemcrm.create_quote_from_template→ Create Quote from Templatecrm.find_quotes→ Find Quotescrm.submit_quote_for_approval→ Submit Quote for Approvalcrm.convert_quote→ Convert Quotecrm.tag_activity→ Tag CRM Activity
- Use picker metadata where supported:
client_id→clientcontact_id→contact, dependent onclient_idwhere appropriateticket_id→ticketuser_id→userquote_id,interaction_id,interaction_type_id, andinteraction_status_idremain UUID fields in v1 unless picker support already exists or is separately introduced.
- Quote action outputs should include enough summary fields for branching: quote ID, number, status, client ID, totals, conversion target IDs, and approval/send state.
Requirements
crm.create_interaction_type
- Register action ID
crm.create_interaction_type, version1. - Inputs:
type_name- optional
icon - optional
display_order - optional
idempotency_key - optional
if_exists:return_existingorerror(defaultreturn_existing)
- Validate non-empty type name and reasonable length.
- Enforce tenant uniqueness by normalized type name where possible.
- If
display_orderis omitted, assign next display order using existinginteraction_typesmax order behavior. - Set
created_byto workflow actor. - Require
settings:updatebecause activity types are tenant CRM taxonomy/configuration. - Return interaction type summary and
createdboolean. - Use action-provided idempotency.
- Write run audit.
crm.update_activity_status
- Register action ID
crm.update_activity_status, version1. - Inputs:
activity_id- either
status_idorstatus_name - optional
reason - optional
no_op_if_already_statusdefaulttrue
- Validate activity exists in tenant.
- Resolve/validate target status from
statuseswherestatus_type = 'interaction'. - If already in target status and no-op is true, return no-op without duplicate audit/event noise.
- Update only
status_idand timestamp/audit metadata as available. - Return activity ID, previous/current status IDs/names,
no_op, and updated activity summary. - Implement as a dedicated wrapper around the same safe update path used by
crm.update_activity.
crm.create_quote
- Register action ID
crm.create_quote, version1. - Scope v1 to quote header creation only. Quote item creation is handled by
crm.add_quote_item. - Inputs should support a constrained, workflow-friendly subset of quote header creation:
client_id- optional
contact_id title- optional
description quote_datevalid_until- optional
po_number - optional
internal_notes - optional
client_notes - optional
terms_and_conditions - optional
currency_codedefaultUSD - optional
idempotency_key
- Validate non-template quotes require client ID.
- Validate
valid_until >= quote_date. - Validate contact belongs to client when provided.
- Use existing quote model/schema behavior where package-boundary safe.
- Return quote summary.
- Require billing create authorization and quote read authorization decisions equivalent to existing quote actions.
- Use action-provided idempotency and run audit.
crm.add_quote_item
- Register action ID
crm.add_quote_item, version1. - Inputs:
quote_iddescriptionquantity- optional
unit_price - optional
unit_of_measure - optional
display_order - optional
phase - optional
is_optionaldefaultfalse - optional
is_selecteddefaulttrue - optional
is_recurringdefaultfalse - optional
billing_frequency - optional
billing_method:fixed,hourly, orusage - optional
is_discountdefaultfalse - optional
discount_type:percentageorfixed - optional
discount_percentage - optional
applies_to_item_id - optional
applies_to_service_id - optional
is_taxabledefaulttrue - optional
tax_region - optional
tax_rate - optional
location_id - optional
cost - optional
cost_currency - optional
idempotency_key
- Validate quote exists in tenant and is editable.
- Reject adding items to quote templates in v1 unless template support is explicitly resolved later.
- Validate line item fields using existing quote item schema rules where package-boundary safe:
- recurring items require billing frequency.
- discount items require discount type.
- percentage discounts require discount percentage.
- Assign
display_orderdeterministically when omitted. - Persist item through shared-safe quote item model/helper behavior.
- Recalculate quote financials after insertion.
- Return quote item summary plus refreshed quote summary/totals.
- Require billing update/read authorization equivalent to existing quote item behavior.
- Use action-provided idempotency and run audit.
crm.create_quote_from_template
- Register action ID
crm.create_quote_from_template, version1. - Inputs:
template_idclient_id- optional
contact_id - optional
titleoverride - optional
quote_dateoverride - optional
valid_untiloverride - optional
po_number - optional
internal_notesoverride/append decision if supported safely - optional
client_notesoverride/append decision if supported safely - optional
currency_codeoverride if supported safely - optional
idempotency_key
- Validate
template_idpoints to a tenant quote template (is_template = true). - Validate target client exists and contact belongs to that client when supplied.
- Reuse/extract shared-safe behavior from existing quote template creation logic; do not import
withAuthserver action wrappers directly into shared runtime. - Copy safe template header fields and template items to the new quote.
- Apply supplied overrides after template defaults.
- Recalculate quote financials after copying items.
- Return created quote summary and created item summaries.
- Require billing create/read authorization equivalent to existing quote template creation behavior.
- Use action-provided idempotency and run audit.
crm.find_quotes
- Register action ID
crm.find_quotes, version1. - Side-effect-free.
- Inputs:
- optional
quote_id - optional
quote_number - optional
client_id - optional
status - optional
date_from - optional
date_to - optional
is_templatedefaultfalse - pagination and sorting fields aligned with
Quote.listByTenant - optional
on_empty:return_emptyorerror
- optional
- Require at least one meaningful filter or an explicit bounded date range unless a small page size is enforced.
- Enforce billing read authorization and quote authorization-kernel filtering equivalent to existing quote list/read behavior.
- Return paginated quote summaries and first quote.
crm.submit_quote_for_approval
- Register action ID
crm.submit_quote_for_approval, version1. - Inputs:
quote_id- optional
commentorreason - optional
no_op_if_already_pendingdefaulttrue
- Validate quote exists, is readable/updatable by workflow actor, is not a template, and is in draft status unless no-op applies.
- Transition status to
pending_approvalusing existing quote status rules. - Record quote activity if existing behavior does or if a shared helper is extracted.
- Return quote summary, previous status, new status, and no-op flag.
- Write run audit.
crm.convert_quote
- Register action ID
crm.convert_quote, version1. - Inputs:
quote_idtarget:contract,invoice, orcontract_and_invoice- optional
no_op_if_already_converteddefaulttrue
- Validate quote exists, is not a template, and is eligible for conversion.
- Enforce billing create + update authorization equivalent to existing conversion actions.
- Reuse shared-safe conversion services:
convertQuoteToDraftContractconvertQuoteToDraftInvoiceconvertQuoteToDraftContractAndInvoice
- Validate selected quote items support the requested target and return clear errors from conversion service as workflow action errors.
- Return quote summary plus created contract ID and/or invoice ID.
- Write run audit.
crm.tag_activity
- Register action ID
crm.tag_activity, version1. - Scope v1 to interactions/activities only. Client and contact tagging stay in their own module actions.
- Inputs:
activity_idtags: non-empty array of tag text values- optional
if_exists:no_oporerror(defaultno_op) - optional
idempotency_key
- Validate target interaction/activity exists in tenant.
- Validate tag text using current tag validation rules.
- Create missing tag definitions and insert missing tag mappings idempotently.
- Require CRM interaction/activity update permission and tag create permission when creating new tag definitions, matching existing tag behavior where possible.
- Emit
TAG_DEFINITION_CREATEDfor newly created definitions andTAG_APPLIEDfor newly applied mappings through lazy event publishing with deterministic keys. - Return added/existing tag summaries and counts.
- Use action-provided idempotency and run audit.
Explicitly dropped: crm.create_client_note
- Do not implement
crm.create_client_notein this follow-up plan. - Use
clients.add_notefor client note automation. - Add a future
contacts.add_noteContact-module action if workflows need contact note automation. - Rationale: module-specific note actions keep ownership clearer and avoid duplicating the newly merged Client workflow note behavior under CRM.
Cross-cutting Requirements
- Implement in
shared/workflow/runtime/actions/businessOperations/crm.tsor extracted shared-safe helper modules. - Do not directly import
packages/*/src/actions/*server action wrappers into shared runtime action code. - Use
withTenantTransaction,requirePermission,writeRunAudit,throwActionError, andrethrowAsStandardError. - Use
withWorkflowJsonSchemaMetadatafor picker-backed fields. - Use deterministic idempotency and no-op behavior for retry-sensitive mutations.
- Keep all actions tenant-scoped and actor-attributed.
- Preserve
crm.create_activity_noteand first-pass CRM actions.
Data / API / Integrations
Key files
shared/workflow/runtime/actions/businessOperations/crm.tsshared/workflow/runtime/actions/businessOperations/clients.tsshared/workflow/runtime/actions/businessOperations/tickets.tsshared/workflow/runtime/actions/businessOperations/shared.tsshared/workflow/runtime/jsonSchemaMetadata.tspackages/clients/src/actions/interactionTypeActions.tspackages/clients/src/actions/clientNoteActions.ts(reference only; CRM note wrapper dropped)packages/clients/src/actions/contact-actions/contactNoteActions.ts(reference for futurecontacts.add_note, not this plan)packages/tags/src/actions/tagActions.tspackages/billing/src/actions/quoteActions.tspackages/billing/src/models/quote.tspackages/billing/src/models/quoteItem.tspackages/billing/src/actions/quoteActions.ts(createQuoteFromTemplateas behavior reference; do not import wrapper directly)packages/billing/src/schemas/quoteSchemas.tspackages/billing/src/services/quoteConversionService.tsshared/workflow/streams/domainEventBuilders/tagEventBuilders.tsshared/workflow/streams/domainEventBuilders/crmInteractionNoteEventBuilders.ts
Tables likely touched
interaction_typesinteractionsstatusesquotesquote_itemsquote_activities- quote conversion target tables for contracts/invoices
tag_definitionstag_mappingsclientscontactsaudit_logs
Security / Permissions
crm.create_interaction_typerequiressettings:updatebecause activity types are tenant CRM taxonomy/configuration.- Resolve and document CRM interaction mutation permission for
crm.update_activity_status. - Quote actions, including
crm.add_quote_item, must enforce billing create/read/update and quote authorization-kernel decisions equivalent to existing quote actions. crm.tag_activitymust enforce CRM interaction/activity update permission and tag create permission when creating new definitions.- Workflow actor remains the created_by/performed_by/audit actor unless a future version explicitly supports override.
Observability
- Write run audit for all side-effectful actions.
- Emit existing events where supported:
TAG_DEFINITION_CREATEDTAG_APPLIED
- Quote pipeline actions should emit quote-specific workflow events only when matching
QUOTE_*event schemas/builders already exist. If no matching quote event contract exists, remain audit-only plus existing quote activity records. - Do not invent new
QUOTE_*orINTERACTION_UPDATEDevents in this plan; new event contracts require a separate event-schema plan. - Capture no-op outcomes in action outputs and audit where useful.
Rollout / Migration
- No database migration is expected by default.
- All actions are additive.
- If quote helper extraction changes package exports, keep it backward compatible and covered by existing quote tests.
crm.create_client_noteis dropped in favor of module-specific actions; no migration or runtime registration is needed for it.
Open Questions
All planning decisions for this follow-up scope are resolved. Implementation may still discover package-boundary details that need scratchpad updates.
Acceptance Criteria (Definition of Done)
- Runtime initialization registers the selected follow-up actions at version
1. - Designer catalog shows the selected actions under CRM with labels, schema metadata, and output schemas.
- Interaction type creation is idempotent and permission-checked.
- Activity status update provides a simple validated transition/no-op wrapper.
- Quote create/add-item/template/find/submit/convert actions respect quote lifecycle, billing permissions, and authorization-kernel behavior.
- Quote pipeline actions emit only already-defined quote workflow events; when no matching quote event schema/builder exists, they remain audit-only plus quote activity records.
- CRM activity tagging creates definitions/mappings idempotently for interactions and emits tag events for new changes.
crm.create_client_noteremains absent; client note automation usesclients.add_note, and contact note automation is deferred to a future Contact-module action.- DB-backed tests cover representative happy paths and high-risk guard cases.
- Existing first-pass CRM, Client, Ticket, and quote tests do not regress.