Skip to main content

Work Lifecycle Service

The WorkLifecycleService manages the complete lifecycle of a work entity -- from creation through updates and synchronization to deletion. It is the primary entry point for all CRUD operations on works within the agent package.

Source: packages/agent/src/services/work-lifecycle.service.ts

Overview​

Every work in Ever Works maps to a set of Git repositories (data, markdown, website). The lifecycle service orchestrates creating these repositories, persisting work metadata in the database, and tearing everything down when a work is deleted.

OperationRequired RoleDescription
createWorkAuthenticated userCreates a new work and its backing repositories
updateWorkEditor or higherModifies work metadata and settings
syncFromDataRepositoryEditor or higherPulls latest state from the data repo into the database
deleteWorkOwner onlyRemoves the work and optionally deletes all repositories

Dependencies​

The service injects the following collaborators:

constructor(
private readonly workRepository: WorkRepository,
private readonly dataGenerator: DataGeneratorService,
private readonly markdownGenerator: MarkdownGeneratorService,
private readonly websiteGenerator: WebsiteGeneratorService,
private readonly ownershipService: WorkOwnershipService,
private readonly deployFacade: DeployFacadeService,
)
  • WorkRepository -- Database access for work CRUD operations.
  • DataGeneratorService -- Manages the data Git repository (items, config).
  • MarkdownGeneratorService -- Manages the markdown/README Git repository.
  • WebsiteGeneratorService -- Manages the website Git repository.
  • WorkOwnershipService -- Enforces role-based access checks.
  • DeployFacadeService -- Validates deploy providers (e.g., Vercel).

Creating a Work​

The createWork method accepts a CreateWorkDto and a User context. It performs the following steps:

  1. Extracts fields from the DTO: slug, name, description, owner, readmeConfig, organization, gitProvider, deployProvider.
  2. Persists the work record via workRepository.create().
  3. Sets the computed owner field from dir.getRepoOwner().
  4. Attempts to fetch existing items from the data repository. If items already exist, the generate status is updated to GENERATED.
  5. Returns a success response with the created work entity.
const result = await lifecycleService.createWork(
{
slug: 'my-tools',
name: 'My Tools Work',
description: 'A curated list of developer tools',
organization: false,
gitProvider: 'github'
},
currentUser
);
// result.work contains the persisted entity

Initial State​

When a work is freshly created, its generateStatus is null unless pre-existing items are detected in the data repository, in which case it is set to GENERATED.

Updating a Work​

The updateWork method requires at least Editor role. It performs validation on several optional fields:

FieldBehavior
name, description, owner, organizationMerged with current values (fallback to existing)
readmeConfigReplaces the existing README configuration
deployProviderValidated against DeployFacadeService.getAvailableProviders()
websiteTemplateAutoUpdateToggles automatic website template updates
websiteTemplateUseBetaSwitches between stable and beta template branches; clears websiteTemplateLastCommit on change
communityPrEnabled, communityPrAutoCloseControls community pull request processing

The deploy provider validation checks that the supplied provider ID exists in the list of registered deploy providers. If it does not, a BadRequestException is thrown.

await lifecycleService.updateWork(workId, { name: 'Updated Name', deployProvider: 'vercel' }, currentUser);

Syncing from Data Repository​

The syncFromDataRepository method pulls a snapshot from the data generator and updates the work record accordingly:

  1. Calls dataGenerator.getDataSyncSnapshot() to get the current state.
  2. Updates itemsCount if it has changed. It never touches generateStatus, even when the count drops to zero.
  3. Syncs pull request metadata from the snapshot if the work lacks it.
  4. Syncs readmeConfig (header/footer) from markdown templates when not already configured. The field is written only when the resulting config differs from the stored one (compared canonically: key order, undefined keys and null vs {} do not count).
  5. Persists all accumulated updates in a single workRepository.update() call — or, when nothing differs, writes nothing and answers updated: [] with "Work already up to date.". A kind without a data repository (the App Work) gets the same empty answer without a snapshot.

This method is typically invoked after external changes to the data repository (e.g., manual Git commits), and on every page mount of a Work whose kind has a data repository. POST /api/works/:id/sync-data therefore invalidates the Work's caches and records the work.synced_from_data_repo activity only when updated is non-empty, so an unchanged Work leaves no trace per page view. The sync the page runs when a generation finishes is usually such a no-op, because the generator has already written itemsCount; the Work's caches are cleared for that case by the WorkGenerationCompletedEvent listener (WorkCleanupService.clearWorkCache), which every generation path fires, including Trigger.dev runs through the worker's WorkOperationsService.emitGenerationCompleted call.

Deleting a Work​

The deleteWork method requires Owner role and accepts a DeleteWorkDto that controls which repositories are removed:

FlagDefaultEffect
delete_data_repositorytrueRemoves the data repository via dataGenerator.removeRepository()
delete_markdown_repositorytrueRemoves the markdown repository via markdownGenerator.removeRepository()
delete_website_repositorytrueRemoves the website repository via websiteGenerator.removeRepository()

The deletion flow:

  1. Validates ownership via ownershipService.ensureIsOwner().
  2. Deletes each repository based on the flags, collecting deleted repository names.
  3. Removes the work record from the database.
  4. Runs cleanup operations for all three generators in parallel (dataGenerator.cleanup(), markdownGenerator.cleanup(), websiteGenerator.cleanup()).
  5. Returns a DeleteWorkResponseDto with the list of deleted repositories.
const result = await lifecycleService.deleteWork(
workId,
{
delete_data_repository: true,
delete_markdown_repository: true,
delete_website_repository: false // keep the website repo
},
currentUser
);
// result.deleted_repositories: ['owner/my-tools-data', 'owner/my-tools']

Repository deletion errors for individual repositories are caught and logged but do not prevent the overall deletion from proceeding. Only HttpException errors are re-thrown.

Error Handling​

All methods use the rethrowAsNormalized() utility to ensure consistent error responses. This utility:

  • Re-throws HttpException instances as-is.
  • Wraps other errors in a BadRequestException with a normalized error message.
  • Logs the original error with contextual information.

State Transitions​

[Created] --> generateStatus: null
|
v (items found)
[Created] --> generateStatus: GENERATED
|
v (update / sync)
[Active] --> generateStatus: GENERATED | GENERATING | ERROR
|
v (delete)
[Deleted] --> All repos cleaned up, DB record removed