Organizing Your Project
Start with one file per definition. Add subdirectories when a business domain or process becomes large enough to need them. The examples below are suggestions, not required layouts. See Project structure for the discovery rules shared by every primitive.
Workflows
Grouping workflows by business process keeps their orchestration and
steps easy to navigate. A simple workflow can live in one file; a larger one can have its own
directory. The main file composes the steps, and execution order is defined in the workflow, so
filenames do not need numbering. Actions referenced by a discovered workflow stay in actions/
for action discovery.
Example:
workflows/
├── sendPaymentReceipt.ts
└── invoice-reminder/
├── invoiceReminder.ts
└── steps/
├── loadInvoiceContext.ts
└── composeReminder.ts
Actions
Grouping actions by business object helps readers find what can be done with that object. A subdirectory can group operations on a related object when that relationship is useful for navigation.
Names such as markInvoicePaid or sendReminder describe the business intent. The available
actions follow the operations the application needs; a full set of CRUD actions for every object
is not necessary.
Example:
actions/
└── invoice/
├── markInvoicePaid.ts
├── sendReminder.ts
└── invoice-line/
└── applyDiscount.ts
Connectors
Grouping connectors by external system makes integration code easy to find. An additional product level can help when one provider exposes several products. A custom connector can have its own directory when its definition, client, and types benefit from separate files.
Example:
connectors/
├── payment-gateway.ts
├── acme/
│ ├── accounting.ts
│ └── crm.ts
└── legacy-erp/
├── legacy-erp.ts
├── client.ts
└── types.ts
Datasets
For ingested data, grouping datasets by source makes their origin visible. For derived data, a business domain often gives a more useful grouping. A dataset file describes a table's contract; retrieval and transformation live in syncs and pipelines.
Example:
datasets/
├── legacy-erp/
│ ├── customers.ts
│ └── invoices.ts
└── billing/
└── overdue-invoices.ts
Syncs
Matching sync paths to the datasets they populate makes it easier to move between a table's contract and its ingestion code. Reading and mapping can stay in the sync file until their complexity makes separate helpers useful.
Example:
syncs/
└── legacy-erp/
├── customers.ts
└── invoices/
├── invoices.ts
├── read-invoices.ts
└── map-invoice-row.ts
Pipelines
Source-based grouping works well for provider-specific transformations; domain-based grouping
works well for business processing. A short pipeline can keep its steps
and SQL in one file. Larger queries can be easier to maintain as separate SQL files, with a
sql.ts helper to load or compose them when needed.
Example:
pipelines/
├── normalize-customers.ts
└── billing/
└── overdue-invoices/
├── overdue-invoices.ts
├── sql/
│ ├── unpaid-invoices.sql
│ └── payment-totals.sql
└── sql.ts
Ontology
One file per object type keeps its properties, links, and telemetry
declarations together. Domain directories can help as the ontology grows. A value-types/
directory gives shared value types a recognizable home; a value type
used by one object can stay with that object.
Example:
ontology/
├── customer.ts
├── billing/
│ ├── invoice.ts
│ └── invoice-line.ts
└── value-types/
└── currency-code.ts
Projections
Following the ontology's organization makes projections easy to find
from their target business object. Foreign-key links can stay within the object projection using
.withLinks(...). Dedicated files help distinguish separate link or telemetry projections from
the projection that materializes the object itself.
Example:
projections/
├── customer.ts
└── billing/
├── invoice.ts
├── invoice-line.ts
├── invoice-reviewers.ts
└── invoice-balance-telemetry.ts
Security
Group security files by team or business domain. Groups collect users and service accounts, roles grant access, and membership policies control who can administer a group.
Example:
security/
├── groups/
│ └── billing-team.ts
├── roles/
│ └── billing-access.ts
└── policies/
└── billing-membership.ts
See Authorization for how the definitions work together.
App
Grouping UI code by user-facing feature keeps components and data access close to the screens
that use them. In this layout, each page.tsx composes features for its route, and _features/
holds the feature implementations. The name _features is a choice: the _ prefix is what
excludes the directory from routing.
A small feature can start with one component. Separate components/, utils/, hooks/,
queries.ts, or mutations.ts become useful when there is enough code to group. A sub-feature
can use the same approach as it grows.
Example:
app/
├── layout.tsx
├── invoices/
│ └── page.tsx
└── _features/
└── billing/
├── components/
│ └── InvoiceTable.tsx
├── queries.ts
├── mutations.ts
└── payment-history/
├── components/
│ └── PaymentHistory.tsx
└── queries.ts
See Building Apps for routing, styles, and the available UI packages.