---
title: Extend a Base Entity
description: Add project-specific metadata to the canonical entity shapes (Project, Deal, Company, etc.) from @elevasis/core/entities using the TMeta extension slot.
---
<!-- @generated by packages/sdk/scripts/copy-reference-docs.mjs -- DO NOT EDIT -->
<!-- Regenerate: pnpm scaffold:sync -->


# Extend a Base Entity

Workflows and UI features often operate on domain entities such as projects, deals, companies, or contacts. Rather than each project declaring its own shape from scratch, `@elevasis/core/entities` provides typed base interfaces generic over a `<TMeta>` slot. External projects extend these to add project-specific fields while keeping the canonical shape stable and interoperable with platform tooling.

The canonical demo lives in `core/types/entities.ts` of any scaffold project.

---

## Available Base Entities

Each entity ships as a TypeScript interface, a Zod schema, and an Input type:

| Set                                                            | Description                                 |
| -------------------------------------------------------------- | ------------------------------------------- |
| `BaseProject` / `BaseProjectSchema` / `BaseProjectInput`       | Client or internal project record           |
| `BaseMilestone` / `BaseMilestoneSchema` / `BaseMilestoneInput` | Milestone within a project                  |
| `BaseTask` / `BaseTaskSchema` / `BaseTaskInput`                | Discrete task within a project or milestone |
| `BaseDeal` / `BaseDealSchema` / `BaseDealInput`                | Sales or partnership deal record            |
| `BaseCompany` / `BaseCompanySchema` / `BaseCompanyInput`       | Company / account record                    |
| `BaseContact` / `BaseContactSchema` / `BaseContactInput`       | Individual contact record                   |

All imports come from `@elevasis/core/entities`.

---

## Recipe 1 -- Extend a base entity with custom metadata

Place this in `core/types/entities.ts`. This is the primary pattern -- the scaffold template ships this exact example.

```ts
import { z } from 'zod'
import type { BaseProject, BaseDeal } from '@elevasis/core/entities'
import { BaseProjectSchema, BaseDealSchema } from '@elevasis/core/entities'

// -- Project: extending metadata on a base entity --

export const ProjectMetaSchema = z.object({
  budget: z.number().nonnegative(),
  clientPriority: z.enum(['low', 'medium', 'high'])
})

export type ProjectMeta = z.infer<typeof ProjectMetaSchema>

export const ProjectSchema = BaseProjectSchema.extend({ metadata: ProjectMetaSchema })

export type Project = BaseProject<ProjectMeta>
```

Key points:

- `BaseProjectSchema.extend({ metadata: ... })` merges your metadata Zod schema into the validated shape. Zod handles the rest.
- `BaseProject<ProjectMeta>` infers the TypeScript type with your metadata typed correctly.
- Only the `metadata` field is extended -- all other base fields (`id`, `organizationId`, `name`, `status`, `createdAt`, `updatedAt`, etc.) are inherited unchanged.
- This file is the single source of truth for entity shapes across `core/`, `operations/`, and `ui/`.

---

## Recipe 2 -- Use a base entity as-is

When the base shape covers everything the project needs, skip the extension entirely:

```ts
import type { BaseDeal } from '@elevasis/core/entities'
import { BaseDealSchema } from '@elevasis/core/entities'

export const DealSchema = BaseDealSchema

export type Deal = BaseDeal
```

`BaseDeal` without a type argument defaults `TMeta` to an empty object (`{}`), which Zod validates as `z.object({})`. Use this path when the project has no per-deal custom fields -- you still get the canonical shape and full Zod validation for free.

---

## Recipe 3 -- Reference entity types from a workflow input schema

Workflows that operate on projects or deals should reference the project-local entity types rather than redeclaring the shape. Add this to `operations/src/<workflow>/workflow.ts`:

```ts
import { z } from 'zod'
import type { WorkflowDefinition } from '@elevasis/sdk'
import { ProjectSchema } from '@core/types/entities'

export const myWorkflow: WorkflowDefinition = {
  id: 'my-workflow',
  inputSchema: z.object({
    project: ProjectSchema,
    notes: z.string().optional()
  }),
  // ...
}
```

The `@core/*` alias maps to the `core/` workspace package. This keeps entity contracts in one place and ensures the workflow input type matches whatever the UI passes as the execution payload.

---

## Recipe 4 -- Reference entity types from the UI

UI components accept entity types directly in props. The entity type flows from the `core` package through the component into the workflow execution payload:

```tsx
import type { Project } from '@core/types/entities'

interface ProjectCardProps {
  project: Project
  onExecute?: (project: Project) => void
}

export function ProjectCard({ project, onExecute }: ProjectCardProps) {
  return (
    <button onClick={() => onExecute?.(project)}>
      Run workflow for {project.name}
    </button>
  )
}
```

When wiring this to a `RunResourceButton`, the entity instance becomes the workflow input. See UI Recipes recipe 6 (`Execute a Resource from a Surface`) at `../ui/recipes.md` for the end-to-end pattern where the entity type flows into a resource execution via `RunResourceButton`.

---

## Verification

- **Test core contracts:** `pnpm -C core test` runs the Vitest suite. The template ships `core/types/entities.test.ts` as a working smoke-check -- it `safeParse`s a valid project (expects success) and an invalid one (expects failure). Run this after any schema change.
- **Round-trip safeParse:** Call `ProjectSchema.safeParse(candidateObject)` in a test or REPL to confirm the Zod shape accepts the data your workflows and UI will produce.
- **Cross-package type check:** `pnpm -C ui build` will surface any TypeScript errors if a UI component or hook passes a mismatched entity type to a workflow input.

---

## Cross-references

- [./add-a-resource.md](./add-a-resource.md) -- resource authoring that consumes entity types in `inputSchema`
- [../ui/recipes.md](../ui/recipes.md) recipe 6 -- execute a resource from a surface, with entity-typed input via `RunResourceButton`
- [../reference/contracts.md](../reference/contracts.md) -- auto-generated TypeScript contract shapes for all Organization OS types
