From Turbofy MCP
Use when building or editing a Turbofy website/app — creating an app, syncing it locally, adding/removing/reordering pages, placing or rearranging sections on a page, changing site settings, translating content, fixing URLs/slugs, or pushing changes live. Triggers: 'build my app', 'add a page', 'update the homepage', 'change the layout', 'add a header to every page', 'translate to German', 'fix this URL', 'reorder sections', 'edit my Turbofy app' — even when the user doesn't mention files or tools. Load BEFORE calling any Turbofy_app_* MCP tool. Covers app.ts, pages, section placement, localization, and pull → edit → push. Do NOT use for database schema changes or writing React UI code — load turbofy-platform or turbofy-blocks instead.
How this skill is triggered — by the user, by Claude, or both
Slash command
/turbofy:turbofy-appsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This skill covers building and modifying Turbofy apps — the Apps CMS data model, the `Turbofy_app_*` workflow, the app file layout, localization, and block-instance editing.
This skill covers building and modifying Turbofy apps — the Apps CMS data model, the Turbofy_app_* workflow, the app file layout, localization, and block-instance editing.
For platform-level concerns (workspaces, environments, org/workspace discovery, the data schema, the Turbofy_workspace_* workflow, the data-builder DSL, and core MCP rules like "pass workspaceId explicitly" and "system CMS vs workspace IDs"), see turbofy-platform. The companion skills turbofy-blocks (writing block React components) and turbofy-dynamic-fields (server-side dynamic-field JavaScript) cover those areas in detail.
Load when the user wants to change their site structure or content placement — e.g. "add a contact page", "put the hero above the footer", "translate the site to French". Do not wait for them to mention app.ts, MCP tools, or block-instance IDs.
Apps are how Turbofy lets users assemble web applications from a data schema and a set of building blocks. They can run in two modes:
The runtime, file layout, and Turbofy_app_* workflow are identical for both modes — the difference is the table-visibility constraint that publishing imposes.
This section describes the core tables, their purpose, key fields, and how they relate to each other.
Tables are referenced by name in Turbofy MCP (e.g. Page, Localization).
The source of truth for the Apps CMS app definition is the decompiled workspace files (app.ts and block-types/ under the app directory after Turbofy_app_pull). The MCP writes them to ~/.turbofy/workspaces/<environment>/<workspaceId>/apps/<appId>/. Use them to understand pages, blocks, block types, and macros.
Important: the core Apps CMS tables (App, Page, BuildingBlockType, BuildingBlock, Localization, Image) are system types and are not part of the workspace schema. Their IDs are stable — do not look them up via Turbofy_table_list. Reference them via CmsOfTypeEnum.X inside the DSL (schema.ts, app.ts, record.ts) or via the literal string value ("cmspage", "cmsbuildingblock", …) when calling MCP tools directly. See turbofy-platform § "Core MCP rules" for the full mapping table and the legacy @cms_* migration note.
In dynamic-field code, tables are often referenced by table id (e.g. $$std.getRecord(<tableId>, ...)). For workspace schema tables, resolve IDs via Turbofy_table_list or read them from schema.ts. For system CMS tables, interpolate CmsOfTypeEnum.X into the dynamic-field template literal (the runtime sees the resolved string).
An App is the top-level container for a set of pages.
Key fields:
idnamecreatedAt, updatedAtAn app has many Pages.
A Page represents a route in the webapp (e.g. homepage, product detail, cart).
Key fields:
idnameslugConfig (Json) — config used to resolve/construct route slugs. Format: { "<lang>": { "slug": "/path/[param]" }, "paramsCollectionMap": { "<param>": { "ofType": "<table-ID>", "slugField": "<field>" } } }. For dynamic pages paramsCollectionMap is required — it maps [param] placeholders to collections. ofType must be the table ID (not the table name); use Turbofy_table_list to find it. slugField is optional — when set, the named record field (e.g. "name") is used as a human-readable slug segment.localizedConfig (@dynamic_field, typed as String) — returns page metadata derived from slugConfig and dynamic args. In this workspace it computes:
canonicalPath — the canonical URL path (with slugs resolved)params — resolved entity IDs for dynamic segments (e.g. { articleId: "1234-..." }). These are passed as $$args.params to building block dynamic fields.notFound — true if any referenced entity doesn't existcreatedAt, updatedAtA page belongs to an App and has many BuildingBlocks.
A BuildingBlockType defines a category of building block (e.g. "Navigation", "ProductTable", "Footer"). There are multiple instances of the same block type across the application.
Key fields:
idnamedefaultConfig (@dynamic_field, typed as String) — default configuration for blocks of this type. Use it for static server-side data such as copies, links, and layout settings. It should depend on $$args.lang, not on route params.defaultDynamicData (@dynamic_field, typed as String) — default server-side data for blocks of this type. Use it for data that depends on $$args.params and $$args.searchParams. In SSR apps, this provides the initial data snapshot; later interactions should use hooks.sourceCodeUrl (String) — URL of the block type's source code. Points to a .tsx file (single-file block) or a .source.zip archive (multi-file block).compiledCodeUrl (String) — URL of the compiled/bundled block code.dependencies (Json) — block type's dependency metadata.createdAt, updatedAtA building block type has many BuildingBlocks.
A BuildingBlock is a UI component instance placed on a page.
Key fields:
idname (@dynamic_field) — derived from its BuildingBlockType (here it resolves to the type's name).config (@dynamic_field, typed as String) — configuration for rendering. In this workspace it resolves to the type's defaultConfig.position (String) — determines rendering order within the page. Convention: use multiples of 10 and (optionally) hierarchical positions like 20/10 to render a child block at position 10 inside the block at position 20.dynamicData (@dynamic_field) — server-resolved data for the block. Use it for route-dependent initial data (for example data keyed by $$args.params or $$args.searchParams), especially in SSR apps.createdAt, updatedAtRelationships:
The Localization table stores translated strings for the CMS.
Key fields:
id — follows ${lang}_${kind}_${entityId} (see naming patterns below).dictionary (Json) — translation key/value map. Used by $$std.translate(...) and $$std.batchTranslate(...).content (String) — freeform content (project-specific usage).createdAt, updatedAtNaming patterns (canonical):
| Pattern | Example IDs | Purpose |
|---|---|---|
{lang}_blocktype_{blockTypeId} | en_blocktype_2vaEf74y | Block type copies — written by Turbofy_app_push from record.ts localizations. |
{lang}_block_{blockId} | en_block_ghi789 | Per-instance block overrides. |
{lang}_page_{pageId} | en_page_def456 | Page-level localized content. |
Note: appId is not included in the pattern since localizations already belong to an app via the appId field.
{lang}_blocktype_{blockTypeId} records are managed by the workspace — never edit them directly. They are produced by Turbofy_app_push from each block type's record.ts localizations field, and surfaced back into record.ts by Turbofy_app_pull.
Legacy IDs: older workspaces may contain Localization rows with ad-hoc IDs (en_product, de_footer, en_navigation, en_table, de_search, en_manufacturer_2vaEf74y...). They predate the canonical scheme. Inspect with Turbofy_data_list on the Localization table (ofType: "cmslocalization") and migrate to one of the patterns above when touching them.
The SlugMapping table maps human-readable URL slugs to entity IDs, scoped by type and language.
Key fields:
id — composite key: {appId}#{typeId}#{lang}#{slug} (e.g. 1234-1234-1234-1234#gbqlrk#en#what-to-eat). Mutable — update to change the slug without deleting the record.key (String, @sortby) — composite sort key: {appId}#{typeId}#{lang}#{entityId}#{timestamp} (e.g. 1234-1234-1234-1234#gbqlrk#en#1234-abcd#2026-03-20T17:29:51.407Z). Stable — one record per entity per language.Usage:
eq to find the entity IDTurbofy_data_get by ID to read the slug from keyThe pageLocalizedConfigCode() macro resolves slugs automatically. The engine later passes resolved entity IDs as $$args.params to building blocks.
The Image table stores image assets.
Key fields:
id (ID) — image identifier.url (String) — image URL.width, height (Integer) — dimensions.createdAt, updatedAtThe full Turbofy MCP tool surface and the core MCP rules (workspaceId is explicit, read-before-write, system-CMS-vs-workspace IDs, pagination limits) live in turbofy-platform § 3–4. This section covers only the app-specific tools and the system CMS ofType map.
Turbofy_app_init — create app + default Home page remotely AND scaffold the local workspace in one stepTurbofy_app_pull — pull app into ~/.turbofy/workspaces/<env>/<workspaceId>/apps/<appId>/Turbofy_app_push — compile schema + blocks, upload, reconcile from the unified workspace (supports dryRun: true)Always call Turbofy_app_push with dryRun: true first to preview the diff.
Turbofy_data_*When you need pages/block types/building blocks/localizations/images, use the generic data tools (Turbofy_data_list, Turbofy_data_get, etc. — see turbofy-platform). For the ofType argument, pass the literal string value below — CmsOfTypeEnum.X only works inside the DSL (TypeScript), not in MCP tool calls.
| Entity | ofType (MCP) |
|---|---|
| App | "cmsapp" |
| Page | "cmspage" |
| BuildingBlockType | "cmsbuildingblocktype" |
| BuildingBlock | "cmsbuildingblock" |
| Localization | "cmslocalization" |
| Image (FileDocument) | "filedocument" |
| SlugMapping | "slugmapping" |
See turbofy-platform § 4 for the full mapping table (including the DSL enum equivalents and the less common entries EntityLocalization, Api, Code).
Apps, pages, block types, and block instances are normally managed via the workspace files + Turbofy_app_push — the generic data tools are for one-off reads/edits and for things the workspace doesn't own (e.g. arbitrary Localization rows, SlugMapping entries, FileDocument records).
Turbofy_app_init → Turbofy_app_pull / edit / Turbofy_app_pushFor a brand new app, call Turbofy_app_init. This creates the app + a default Home page remotely and scaffolds the local workspace in one step.
The recommended way to modify an existing app's schema, pages, block types, block instances, and block type source code is the unified workspace:
Turbofy_app_pull writes everything to ~/.turbofy/workspaces/<environment>/<workspaceId>/apps/<appId>/:
schema.ts — GraphQL data schema (DSL)app.ts — schema re-export + page definitions + blocks (DSL); imports block types from ./block-types/index.jsapp.base.json / schema.base.json — serialized baseline used by push for diffing; do not edit by handpackage.json + tsconfig.json — scaffolded Node/TypeScript config so the directory is self-containedblock-types/index.ts — barrel re-exporting every block type variableblock-types/{Name}/record.ts — one record per block type (the appBuilder.blockType({...}) call)block-types/{Name}/index.tsx (+ siblings) — optional runtime React code for the block type, pulled from the .source.zip archive when presentTurbofy_app_push compiles schema, compiles+uploads each block type that has source code on disk, and reconciles the app state.Call Turbofy_app_push with dryRun: true to preview changes before applying them.
Block source location is convention-based: push looks for block-types/{Name}/index.{tsx,ts}. If absent, the block type is treated as sourceless (valid — useful for content-only blocks where the runtime is provided elsewhere). When present, push compiles + uploads it and writes the resulting URLs back to the CMS automatically.
The reconciler diffs the edited file against the current API state and executes only the necessary operations.
After Turbofy_app_pull or Turbofy_app_init, files live under ~/.turbofy/workspaces/<environment>/<workspaceId>/apps/<appId>/:
~/.turbofy/workspaces/<environment>/<workspaceId>/apps/<appId>/
schema.ts # GraphQL data schema (DSL)
app.ts # schema re-export + pages + blocks (DSL); imports from ./block-types
app.base.json # serialized baseline app state (do not edit)
schema.base.json # serialized baseline schema declaration (do not edit)
package.json # scaffolded so the directory is self-contained
tsconfig.json
block-types/
index.ts # barrel: re-exports every block type variable
Navigation/
record.ts # one row of the BuildingBlockType table, in code
index.tsx # runtime React code (optional)
ProductTable/
record.ts
index.tsx
Dashboard/ # multi-file block type
record.ts
index.tsx # entry point
helpers.ts
chart-utils.ts
Footer/
record.ts # sourceless block type — record only, no index.tsx
<environment> is the active MCP environment (alpha or prod) — the turbofy MCP uses prod, turbofy-alpha uses alpha. <workspaceId> and <appId> are the IDs reported by Turbofy_list_workspaces and Turbofy_app_pull / Turbofy_app_init.
record.ts is a per-block-type manifest that maps 1:1 to a row in the BuildingBlockType table. It imports appBuilder and exports a single const:
// block-types/Navigation/record.ts
import { appBuilder } from "@graphapi-io/dsl-builders";
export const navigationBlock = appBuilder.blockType({
id: "...",
name: "Navigation",
defaultConfig: `...`,
});
record.ts does not belong to the runtime bundle. The block's index.tsx must not import it — push and the iframe runtime keep them separate by design.
block-types/index.ts is a generated barrel that re-exports each block type from its sibling record.ts:
// block-types/index.ts
export { navigationBlock } from "./Navigation/record.js";
export { dashboardBlock } from "./Dashboard/record.js";
export { footerBlock } from "./Footer/record.js";
In app.ts, block types are imported from the barrel and used in page definitions:
export { schema } from "./schema.js";
import { appBuilder } from "@graphapi-io/dsl-builders";
import {
navigationBlock,
dashboardBlock,
footerBlock,
} from "./block-types/index.js";
const home = appBuilder.page({ ... });
export const app = appBuilder.buildApp({
name: "My App",
i18n: { locales: ["en", "de"], default: "en" },
pages: [home, productDetail],
blockTypes: [navigationBlock, dashboardBlock, footerBlock],
});
Localized copies for block types live inside the workspace, alongside the block type definition. The agent edits them directly in record.ts; Turbofy_app_push materializes them into Localization rows in the CMS, and Turbofy_app_pull reverses the round trip.
i18n on buildApp — the source of truth for supported localesEvery workspace declares its supported languages in buildApp({ i18n: ... }). This is the only place that lists locales for the app.
export const app = appBuilder.buildApp({
name: "My App",
i18n: {
locales: ["en", "de"], // every locale the app supports
default: "en", // root-locale redirect target AND copies-fallback locale
},
pages: [home, productDetail],
blockTypes: [navigationBlock, dashboardBlock, footerBlock],
});
Rules enforced by buildApp:
i18n.locales must be non-empty.i18n.default must appear in i18n.locales.localizations keys must be a subset of i18n.locales. Adding a key for an unsupported locale is a hard error — the only way to localize into a new language is to first add it to i18n.locales.Push reconciles i18n into App.settings.i18n on the server. Pull reads it back from there. There is no other place to declare locales.
i18n.defaultplays two runtime roles: (1) it controls the root-locale redirect, and (2) it is the locale used as the fallback dictionary for auto-injectedcopies(see "Auto-injectedcopies" below). They are intentionally the same value — there is no separate "fallback" knob.
localizations in record.tsEach record.ts carries a localizations field — a record keyed by language code, whose values are flat dictionaries of copies:
// block-types/Navigation/record.ts
import { appBuilder } from "@graphapi-io/dsl-builders";
export const navigationBlock = appBuilder.blockType({
id: "blktype_2vaEf74y",
name: "Navigation",
defaultConfig: `({ columns: 3 });`,
localizations: {
en: { home: "Home", about: "About", contact: "Contact" },
de: { home: "Startseite", about: "Über uns", contact: "Kontakt" },
},
});
At push time each non-empty entry becomes a ${lang}_blocktype_${blockTypeId} Localization row. At runtime the dictionary for the current locale is exposed as config.copies to the React component (see the turbofy-blocks skill).
Auto-injected
copies(hard lock). When you setdefaultConfigon a block type, the builder wraps your code so the runtime always returns{ ...yourResult, copies: <localized dictionary> }. You never need to spreadblockTypeCopiesCode()yourself — and even if you tried to set acopieskey in your object, it would be overridden. Same for block-instanceconfig: any user-supplied code is wrapped to always emitcopiesas the merge of the block type's translations and the block instance'slocalizationsoverrides. OnlydefaultConfigandconfigare wrapped —defaultDynamicDataanddynamicDataare left untouched. The wrap is reversible:Turbofy_app_pullstrips it back to your original source.Fallback-language merge. The wrap also fetches the dictionary for the app's
i18n.defaultlocale ("en"wheni18nis unset) and merges it under the active locale. Any key missing in the active locale falls back to the default locale's value, so a half-translated locale degrades gracefully instead of returningundefined.For block instances the merge order is language tier first, scope tier second — every active-locale dict beats every fallback-locale dict; within a tier, block-instance beats block-type. From lowest → highest priority: block-type default → block-instance default → block-type active → block-instance active. This guarantees that translating a block type into the active locale wins over a default-locale-only block-instance override (otherwise the German page would render the English instance copy instead of the translated type copy).
The default locale is baked into the wrap at
Turbofy_app_pushtime fromi18n.default, so changing it requires a re-push.Single-batch fetch. All required dictionaries (2 for block-type
defaultConfig, 4 for block-instanceconfig) are pulled in a single$$std.batchGetRecordsByInputs(...)call. That means one runtime interruption per dynamic field regardless of how many locales are merged — and the runner internally dedupes by(ofType, id)so a request where active === fallback collapses to a single underlying read.
Turbofy_app_pull always seeds every locale declared in i18n.locales into each block type's localizations, even when the CMS has no data for that locale. You will see entries like:
localizations: {
en: { joinAs: "You are", roomLabel: "Room" },
de: { joinAs: "", roomLabel: "" },
}
The empty de strings are a contract: this app supports German, this block type has not been translated yet, and these are the keys that need translations. Do not delete empty per-locale entries — that would silently drop the language from the contract for that block type. Instead, fill in the translations.
If a workspace pull surfaces a locale you didn't expect (e.g. fr: { ... } while i18n.locales is ["en", "de"]), it means a stray Localization row exists in the CMS. Pull preserves it so it doesn't get silently dropped, but buildApp will reject the push until you either add "fr" to i18n.locales or remove the locale from the block type.
i18n.locales in buildApp (and update default if needed).Turbofy_app_push. This writes the updated App.settings.i18n to the server.Turbofy_app_pull. Every block type's record.ts now has an empty dictionary for the new locale, ready to fill in.Turbofy_app_push again to materialize the localizations.appBuilder.block(...)Individual block instances can override the type's copies by setting localizations on the block(...) call inside a page. The shape mirrors block-type localizations — a record keyed by language code with flat dictionaries — but with two important differences:
localizations unset. Add it only when this particular instance on this particular page needs different copy than the default.Turbofy_app_pull does not auto-seed empty dictionaries here. Pull writes localizations only when at least one CMS override actually exists. This keeps page files clean — overrides stand out instead of drowning in empty placeholders.// app.ts
const home = appBuilder.page({
name: "Home",
blocks: [
appBuilder.block({
type: navigationBlock,
localizations: {
en: { home: "Home" },
de: { home: "Zuhause" }, // override only the keys that differ
},
}),
// No `localizations` — inherits everything from `navigationBlock.localizations`.
appBuilder.block({ type: footerBlock }),
],
});
Push materializes each non-empty dictionary into a ${lang}_block_${blockId} Localization row. Validation in buildApp enforces the same locale subset rule as block types: every key under localizations must be in i18n.locales.
To remove an override, delete the localizations field (or the specific locale entry) and run Turbofy_app_push. The reconciler does not delete dropped Localization rows automatically — use Turbofy_data_delete on the Localization row (ofType: "cmslocalization") if you need the CMS row gone, otherwise the empty/stale row is harmless.
record.ts localizations. The builder auto-injects them into config.copies at runtime regardless of what you put in defaultConfig. This is where almost all UI strings belong.localizations on the corresponding appBuilder.block(...) call in app.ts (writes ${lang}_block_${blockId} rows on push). Or, for ad-hoc one-off edits, use Turbofy_data_create / Turbofy_data_update with ofType: "cmslocalization".{lang}_page_{pageId}) — page metadata (title, description) consumed by pageLocalizedConfigCode(). Manage via Turbofy_data_* with ofType: "cmslocalization".de" use case). Use ad-hoc dictionaries the entity's dynamic fields can read via $$std.translate(...).When app work requires schema changes (new tables, fields, enums), edit schema.ts at the workspace root — Turbofy_app_push reconciles schema changes as a side-effect of the app push, so you don't need a separate Turbofy_workspace_push. For pure schema work outside an app context, use the Turbofy_workspace_pull → edit → Turbofy_workspace_push loop directly. The schema DSL itself (builder calls, field types, parents, critical rules) is documented in turbofy-platform § 5–6.
The app.ts file imports macros from @graphapi-io/declarations:
import { macros } from "@graphapi-io/declarations";
const {
blockConfigCode,
blockDynamicDataCode,
blockNameCode,
blockTypeCopiesCode,
pageLocalizedConfigCode,
} = macros;
| Macro | Used in | Description |
|---|---|---|
blockTypeCopiesCode() | internal | Resolves localized copies for the block type from ${lang}_blocktype_${$$self.id} Localization records. Used as the schema default for BuildingBlockType.defaultConfig (when no user code is provided). Do not spread it into defaultConfig yourself — the dsl-builders auto-inject copies into every user-supplied defaultConfig (hard lock; see "Auto-injected copies"). |
blockConfigCode(BlockTypeTable.id) | internal | Inherits the parent block type's defaultConfig and merges block-instance copies. Used as the schema default for BuildingBlock.config. User-supplied config is wrapped automatically (the wrap merges block-type + block-instance translations into copies); you do not need to call this macro from app.ts. |
blockDynamicDataCode(BlockTypeTable.id) | BuildingBlock.dynamicData | Inherits the parent block type's defaultDynamicData. Used in the schema definition and in per-instance dynamicData overrides to extend parent data. |
blockNameCode(BlockTypeTable.id) | BuildingBlock.name | Derives the block instance name from its block type. Used in the schema definition. |
pageLocalizedConfigCode() | Page.localizedConfig | Computes localized page metadata (canonical path, notFound). Used in the schema definition. |
block() call inside a page's blocks array.block() call.position.config or dynamicData string on the block.Example — adding a block to a page:
const home = appBuilder.page({
id: "existing-page-id",
name: "Home",
blocks: [
appBuilder.block({ id: "existing-block-id", type: hero }),
appBuilder.block({ type: card, config: "({ title: 'New card' })" }), // NEW — no id means create
],
});
Blocks without an id in the DSL file are treated as new (create). Blocks with an id that no longer appear are deleted.
Most localization edits happen in the workspace, not via MCP data tools:
block-types/{Name}/record.ts localizations, then Turbofy_app_push. See "Localization workflow" above.localizations on the relevant appBuilder.block(...) call in app.ts, then Turbofy_app_push. See "Per-block instance overrides" above.i18n.locales in buildApp, then Turbofy_app_push.Use the generic data tools below for ad-hoc edits or things the workspace doesn't own:
Turbofy_data_list with ofType: "cmslocalization" — list localizations (filter the returned items client-side by id prefix, e.g. starting with en_page_<pageId>)Turbofy_data_get with ofType: "cmslocalization" — fetch a localization recordTurbofy_data_create / Turbofy_data_update — create or update a localization recordThese are the right tools for page-level content ({lang}_page_{pageId}) and one-off per-block instance edits when you don't want to round-trip through the workspace. Avoid using them to write {lang}_blocktype_{blockTypeId} rows directly — those are owned by the workspace and the next Turbofy_app_pull will reflect whatever is in record.ts. The same applies to {lang}_block_{blockId} rows when the block has localizations declared on the block(...) call in app.ts: pull will surface them and push will overwrite anything you wrote outside the workspace.
When a block is created via Turbofy_app_push, an empty en_block_<blockId> Localization row is created as a side-effect (so the editor UI has something to write to). It is harmless — pull skips empty dictionaries and the workspace remains the source of truth.
Use the generic data tools to read CMS entities:
Turbofy_data_list / Turbofy_data_get with ofType: "cmspage"Turbofy_data_list / Turbofy_data_get with ofType: "cmsbuildingblocktype"Turbofy_data_list / Turbofy_data_get with ofType: "cmsbuildingblock"For block instances on a specific page, list with ofType: "cmsbuildingblock" and filter client-side by pageId.
Creating an app from scratch and modifying an existing one are the same
workflow with two switches: Turbofy_app_init vs Turbofy_app_pull at the
front, and a full build vs a delta in the middle. Once the schema and a per-block
contract are fixed, blocks are largely independent of each other, so the block
work parallelizes — but the pieces within one block (dynamic field →
record.ts → index.tsx) are a dependency chain that must stay together in one
worker.
Scale to the work. For one or two blocks, just do it inline. Fan out only when there are several independent blocks to build/modify — parallelism buys wall-clock at the cost of N× context.
Parallel workers must write disjoint files. The shared files have exactly one owner: the orchestrator (the main agent driving this skill).
| Owner | Writes | Never writes |
|---|---|---|
| Orchestrator (main agent) | app.ts, schema.ts, the block-types/index.ts barrel; runs init/pull and the single push | individual block-types/<Name>/ internals |
turbofy-block-builder (one per block) | only its own block-types/<Name>/ (record.ts, index.tsx) | app.ts, the barrel, schema.ts, other blocks; never pushes |
turbofy-schema-builder (optional) | schema.ts | app.ts, block-types/ |
The fan-out happens at the orchestrator (main-agent) level — a subagent can't spawn further subagents. If custom plugin agents aren't available in your environment, brief a general subagent per block with the same lane rules and the
turbofy-blocks+turbofy-dynamic-fieldsskills loaded.
Turbofy_app_init (from scratch) or Turbofy_app_pull
(existing). Confirm the app directory exists on disk.schema.ts (inline, or delegate to
turbofy-schema-builder). You usually do not push schema here — the final
Turbofy_app_push reconciles it. Push early only if a builder needs live
data. Capture the resulting data contract (table names, IDs, fields).config knobs + copy keys + locales, the dynamicData shape and which schema
tables/fields it reads, sourceless-or-React, and target page/position. This is
the sequential, high-leverage step that stops parallel builders from drifting.
For a delta, label each block add / modify / delete.turbofy-block-builder per
added or modified block, each with its contract. Deletes are not
workers — remove the block-types/<Name>/ dir and unwire it yourself.
Collect each builder's returned barrel line, import name, instance config,
assumed schema deltas, and copy keys.block-types/index.ts barrel; wire
imports, appBuilder.block(...) placement, positions, and pages into
app.ts. Keep i18n.locales a superset of every locale the builders used.Turbofy_app_push with dryRun: true, fix any cross-block
errors (re-delegating to the relevant builder if a single block is at fault),
then the real push. This is a hard barrier — never run parallel or
per-block pushes; the reconciler operates on the whole app from disk.Within a block, the React component depends on the block-type DSL, which often
depends on the dynamic-field code — so that vertical slice lives in one
turbofy-block-builder and runs in order. Across blocks there's no such
dependency once the schema + contracts are fixed, which is exactly the axis you
parallelize on.
turbofy-platform — platform orientation, workspaces & environments, org/workspace discovery, the full MCP tool surface + core rules, the workspace schema workflow (Turbofy_workspace_*), and the data-builder DSL.turbofy-blocks — writing block-type runtime React components (block-types/<Name>/index.tsx): props, copies, UI/UX rules, client-side hooks.turbofy-dynamic-fields — server-side dynamic-field code (defaultConfig, defaultDynamicData, page localizedConfig): the $$std API, $$args, $$self, reserved dynamicArgs keys.npx claudepluginhub graphapi-io/turbofy-ai-plugin --plugin turbofyCreates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.