Skip to content
bitzorcas
中EN

Concept

Guidance campaigns, assignment, and progress

Source-verified Guidance Campaigns — the tenant campaign aggregate root, the Draft/Active/Paused/Archived state machine, user assignment and progress, audience and scheduling, server-authoritative completion and dismissal, plus analytics, error codes, and persistence boundaries.

Last updated

A campaign organizes published GuidanceContent into an ordered, audience-scoped, scheduled multi-step guided path and tracks independent progress per user. It is not the same as contextual guidance. Contextual guidance is “show help when a user reaches a page.” A campaign is “an operator actively publishes a set of steps to guide a class of users along a path.”

1. Two aggregate roots

ordered stepsaudience + schedule

GuidanceCampaign
tenant campaign

published GuidanceContent

pre-assigned on activate
implicitly assigned on first interaction

GuidanceCampaignAssignment
user assignment and progress

NotStarted → InProgress
→ Completed / Dismissed

GuidanceCampaign is the tenant-level aggregate root. It combines an ordered list of published GuidanceContent step IDs with audience-targeting rules and a scheduling window. GuidanceCampaignAssignment is the user-level assignment-and-progress aggregate root, unique on (TenantId, CampaignId, UserId) (index UX_GuidanceCampaignAssignment_Tenant_Campaign_User).

2. Campaign state machine

GuidanceCampaignStatus has four states:

StateMeaning
DraftUnder editing, not visible to end users
ActiveActivated, visible to users matching audience and schedule
PausedDelivery paused, assigned progress is retained
ArchivedArchived, no longer delivered

Transitions are strictly guarded by the aggregate. A violation returns the corresponding Guidance.Campaign.* error:

  • Create produces Draft;
  • Configure (steps, audience, schedule) is allowed only in Draft or Paused, otherwise Guidance.Campaign.MustBeEditable;
  • Activate is allowed only from Draft or Paused, and rejects an expired schedule (Guidance.Campaign.ScheduleExpired);
  • Pause is allowed only from Active (Guidance.Campaign.TransitionInvalid);
  • Archive is allowed from any non-archived state.

Every lifecycle change must carry a LastChangeReason (max 512 characters) as a bounded audit field.

3. Audience and schedule

Audience targeting does not rely on a single role string. It combines these fields:

FieldLimitSemantics
TargetRoleNamesat most 50matching any role qualifies
TargetUserIdsat most 200explicitly named users
TargetsAllUserscomputedtrue when both are empty, meaning the whole tenant

IsEligible(userId, roles, now) combines state, scheduling window, and role/user membership into one decision. Scheduling is controlled by StartsAt (null means immediate) and EndsAt (null means no automatic expiry). The database CHECK constraint CK_GuidanceCampaign_Schedule enforces StartsAt < EndsAt. Activating a campaign whose window has already ended is rejected with Guidance.Campaign.ScheduleExpired.

On activation, the system pre-creates assignments for explicitly named target users (EnsureExplicitAssignmentsAsync). Campaigns targeting the whole tenant or by role create assignments implicitly on first user interaction (CreateAssignmentAsync).

4. Ordered multi-step and progress state machine

Steps are an ordered ContentIds list, capped at StepLimit = 20, each identifier at most 64 characters. User progress is described by GuidanceProgressStatus:

StateMeaning
NotStartedAssigned but not started
InProgressAt least one step completed, not all
CompletedAll steps completed
DismissedUser dismissed

Progress is server-authoritative, not self-reported by the client:

  • CompleteStep is idempotent. Completing any step for the first time moves NotStarted to InProgress; once all steps are completed it advances to Completed. Completing the same step again does not error. Completing an already-completed campaign returns Guidance.Campaign.AlreadyCompleted.
  • Dismiss is idempotent but controlled by the campaign-level AllowDismiss. When AllowDismiss is false it returns Guidance.Campaign.DismissDenied; dismissing an already-dismissed campaign returns Guidance.Campaign.AlreadyDismissed.
  • A missing step returns Guidance.Campaign.StepNotFound.

This “server-authoritative plus idempotent plus AllowDismiss” chain keeps progress trustworthy: a client cannot fabricate completion or bypass the dismissal policy.

5. HTTP routes

Campaign routes are grouped under /api/v1/guidance/campaigns and source-generated:

Method and routePurposePermission
POST /api/v1/guidance/campaignscreate campaignguidance.campaign.manage
GET /api/v1/guidance/campaignsadministration listguidance.campaign.read
GET /api/v1/guidance/campaigns/{campaignId}campaign detailguidance.campaign.read
PUT /api/v1/guidance/campaigns/{campaignId}update steps/audience/scheduleguidance.campaign.manage
POST /api/v1/guidance/campaigns/{campaignId}/activateactivateguidance.campaign.manage
POST /api/v1/guidance/campaigns/{campaignId}/pausepauseguidance.campaign.manage
POST /api/v1/guidance/campaigns/{campaignId}/archivearchiveguidance.campaign.manage
GET /api/v1/guidance/campaigns/{campaignId}/analyticscompletion rate and distribution analyticsguidance.campaign.read
GET /api/v1/guidance/campaigns/mecampaigns visible to the current userguidance.campaign.use
POST /api/v1/guidance/campaigns/me/{campaignId}/steps/{contentId}/completecomplete a stepguidance.campaign.use
POST /api/v1/guidance/campaigns/me/{campaignId}/dismissdismiss a campaignguidance.campaign.use

Administration operations (create, update, state transitions, read, analytics) use guidance.campaign.read / guidance.campaign.manage. End-user operations (view own campaigns, complete a step, dismiss) use guidance.campaign.use. The three permission classes are independent: operators cannot accidentally use end-user permissions, and end users cannot gain administration power.

6. Analytics definition

GET /api/v1/guidance/campaigns/{campaignId}/analytics returns GuidanceCampaignAnalytics with actual (non-estimated) progress distribution counts: NotStarted, InProgress, Completed, Dismissed, plus CompletionRate. These numbers come from the real state of assignment aggregates, not from sampling or estimation. A console can render a funnel directly from them.

7. Persistence

The SQL Server migration 202607300004-guidance-campaigns.sql creates two tables:

  • dbo.GuidanceCampaign, with CK_GuidanceCampaign_Status ('Draft','Active','Paused','Archived') and CK_GuidanceCampaign_Schedule, indexed by Tenant/Status and Tenant/Language.
  • dbo.GuidanceCampaignAssignment, with CK_GuidanceCampaignAssignment_Status ('NotStarted','InProgress','Completed','Dismissed') and the unique index UX_GuidanceCampaignAssignment_Tenant_Campaign_User, plus a query index on Tenant/Campaign/Status.

GuidanceCampaignStore implements IGuidanceCampaignStore in the Infrastructure layer and backs the commands and queries above. Guidance Campaign has the same SqlSugar/EF Core parity foundation as GuidanceContent and Session.

8. Difference from contextual guidance

Two concepts are easy to confuse:

  • Contextual guidance (GuidanceContent + Contextual Query): passive. When a user reaches a route/component, a four-level match returns one published help entry. Audience is a single RequiredRole string.
  • A campaign (GuidanceCampaign): active. An operator strings several published items into ordered steps, targets by role/user/whole-tenant, and carries scheduling with server-authoritative progress.

Both share GuidanceContent as the content source, but a campaign only references published content IDs and never copies the body. The content publication lifecycle (Draft/Published) and the campaign lifecycle (Draft/Active/Paused/Archived) are therefore independent. Unpublishing content referenced by a campaign affects the readability of that campaign step.

9. Error code reference

Campaign error codes live in the Guidance.Campaign.* namespace. Representative entries:

Error codeTrigger
Guidance.Campaign.Invalidinput validation failed
Guidance.Campaign.TenantRequiredmissing trusted tenant context
Guidance.Campaign.NotFoundcampaign does not exist
Guidance.Campaign.ContentInvalidstep content reference is invalid
Guidance.Campaign.MustBeEditableConfigure run in a non-editable state
Guidance.Campaign.TransitionInvalidillegal state transition
Guidance.Campaign.ScheduleExpiredactivating an expired schedule
Guidance.Campaign.NotEligibleuser does not match the audience
Guidance.Campaign.StepNotFoundstep does not exist
Guidance.Campaign.AlreadyDismissedrepeated dismissal
Guidance.Campaign.DismissDeniedcampaign does not allow dismissal
Guidance.Campaign.AlreadyCompletedrepeated completion
Guidance.Campaign.VersionConflictoptimistic concurrency conflict

10. Source review

Terminal window
# Campaign aggregate, state machine, assignment, and progress.
rg -n "GuidanceCampaign|GuidanceCampaignStatus|GuidanceProgressStatus|GuidanceCampaignAssignment" \
src/Platform/Guidance -g '*.cs'
# Campaign permission codes and routes.
rg -n "guidance.campaign|CampaignResource|HttpRoute" \
src/Platform/Guidance -g '*.cs'
# Physical tables and constraints.
rg -n "GuidanceCampaign|CK_GuidanceCampaign" \
src/Hosts/BitzOrcas.Api/SchemaMigrations/SqlServer/202607300004-guidance-campaigns.sql

Guidance overview · Content lifecycle and persistence · Contextual selection and authorization

100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%