Keep every implementation as small and obvious as possible.
- Simplicity first – Prefer the simplest data structures and APIs that work
- Avoid needless abstractions – Refactor only when duplication hurts
- Remove dead code early –
pnpm tidyscans for unused files/deps and lets you delete them in one command - Minimize dependencies – Before adding a dependency, ask "Can we do this with what we already have?"
- Consistency wins – Follow existing naming and file-layout patterns; if you must diverge, document why
- Explicit over implicit – Favor clear, descriptive names and type annotations over clever tricks
- Fail fast – Validate inputs, throw early, and surface actionable errors
- Let the code speak – If you need a multi-paragraph comment, refactor until intent is obvious
Giselle is built to design and run AI workflows beyond prompt chains. Not a chat. Not a chain. A system you can run.
- Visual editor
- Instant execution
- No infra headaches
- Open source — self-host or use our cloud
Giselle uses a Turborepo monorepo with pnpm workspaces, organized into four main directories:
/workspace
├── apps/ # Deployable applications
│ ├── studio.giselles.ai/ # Giselle Cloud (production)
│ └── ui.giselles.ai/ # UI component showcase
├── packages/ # Published SDK packages (@giselles-ai/*)
├── internal-packages/ # Internal shared packages (@giselle-internal/*)
└── tools/ # Development utilities
SDK Packages (packages/@giselles-ai/*):
protocol— Core domain types and schemas (Workspace, Node, Task, Generation)giselle— Engine implementation (tasks, generations, triggers, integrations)react— React hooks and components for client integrationnextjs— Next.js integration with route handlerslanguage-model— Language model abstractions and cost calculationslanguage-model-registry— Provider-specific model implementationsrag— RAG pipeline (chunking, embedding, querying)github-tool— GitHub integration utilities
Internal Packages (internal-packages/@giselle-internal/*):
workflow-designer-ui— Visual workflow editor (React Flow-based)ui— Shared UI components (Radix-based)
| Layer | Technology |
|---|---|
| Runtime | Node.js 22+ |
| Package Manager | pnpm 10+ |
| Build System | Turborepo |
| Web Framework | Next.js 16 (App Router) |
| UI Library | React 19 |
| Styling | Tailwind CSS 4 |
| State Management | Zustand (editor), SWR (data fetching) |
| Validation | Zod v4 |
| Database | PostgreSQL with Drizzle ORM |
| Vector Store | pgvector |
| Formatting/Linting | Biome |
| Testing | Vitest |
| AI SDK | Vercel AI SDK |
Workspace (JSON) → Protocol Types → Giselle Engine → Task Execution → Generation Output
↓
Language Model Registry → AI Provider APIs
- Workspace — A visual workflow containing nodes and connections
- Node — Either an OperationNode (actions, text generation, triggers) or VariableNode (text, files, vector stores)
- Task — An executable instance of a workflow with sequences of generations
- Generation — A single step execution (created → queued → running → completed/failed)
- App — A published workflow entry point with parameters
TBD
pnpm install # Install all dependencies
pnpm build-sdk # Build SDK packages (required before running apps)# Development
pnpm dev:studio.giselles.ai # Start Giselle Cloud
# Build
pnpm build-sdk # Build SDK packages
pnpm -F studio.giselles.ai build # Build Giselle Cloud
# Quality Checks
pnpm format # Format code with Biome
pnpm check-types # Type-check all packages
pnpm test # Run all tests
pnpm tidy # Find unused files/dependencies
pnpm tidy --fix # Remove unused files/dependenciesRun these commands in order:
pnpm format— Format codepnpm build-sdk— Rebuild SDK packagespnpm check-types— Verify typespnpm tidy— Check for unused codepnpm test— Run tests- Update
.continuity/per-branch ledger — Reflect the change immediately
When adding a new public API to packages/giselle/src/giselle.ts, also add the corresponding routing entry to packages/http/src/router.ts (typically jsonRoutes.<name> using giselle.<name>.inputSchema) so the API is reachable through the HTTP layer (e.g., via NextGiselle).
pnpm test # Run all tests
pnpm -F @giselles-ai/giselle test # Run tests for a specific package
cd packages/giselle && vitest # Run tests in watch mode
vitest run src/tasks/run-task.test.ts # Run a specific test fileTest files follow the *.test.ts naming pattern and use Vitest.
- Create PRs in meaningful minimum units — even 1 commit or ~20 lines is fine
- Feature flags protect unreleased features, so submit PRs for any meaningful work
- ~500 lines: Consider wrapping up for a PR
- 1000 lines: Maximum threshold — avoid exceeding this
TBD File Names: kebab-case
✅ user-profile.ts
✅ api-client.tsx
✅ text-generation.ts
❌ UserProfile.ts
❌ apiClient.tsx
Components: PascalCase
✅ UserProfile
✅ TextGenerationNode
❌ userProfile
Variables and Functions: camelCase
✅ userEmail
✅ calculateTotalPrice()
✅ validateUserInput()
❌ user_email
Booleans: Prefix with is, has, can, should
✅ isEnabled, hasPermission, canEdit, shouldRetry
✅ isCompletedGeneration(), hasActiveSubscription()
❌ enabled, permission, completed
ID Types: Prefixed strings with branded types
// packages/protocol/src/node/base.ts
export const NodeId = createIdGenerator("nd"); // "nd_xxx"
export const InputId = createIdGenerator("inp"); // "inp_xxx"
export const OutputId = createIdGenerator("otp"); // "otp_xxx"TBD Formatting (Biome)
- Tab indentation
- Double quotes for strings
- Organized imports (auto-sorted)
TypeScript
- Prefer explicit types over
any - Use Zod schemas for runtime validation and type inference:
export const Workspace = z.object({ id: WorkspaceId.schema, nodes: z.array(NodeLike), connections: z.array(Connection), }); export type Workspace = z.infer<typeof Workspace>;
- Use discriminated unions for variant types:
export const Node = z.discriminatedUnion("type", [ OperationNode, VariableNode, ]);
React
- Functional components with hooks
- Zustand for complex state (editor store)
- SWR for server data fetching
- Selective subscriptions to minimize re-renders:
// Good: Subscribe only to needed data const node = useEditorStore((s) => s.nodesById[nodeId]); // Bad: Subscribe to entire state const state = useEditorStore((s) => s);
Async/Await
- Prefer async/await over raw promises
- Use try/catch for error handling
TBD Custom Error Classes with Symbol Markers
For cross-package error identification, use Symbol-based instance checking:
const marker = "giselle.react.error.APICallError";
const symbol = Symbol.for(marker);
export class APICallError extends ReactError {
private readonly [symbol] = true;
static isInstance(error: unknown): error is APICallError {
return ReactError.hasMarker(error, marker);
}
}Validation Errors
Handle Zod validation errors at API boundaries:
try {
return await jsonRoutes[routerPath](giselle)({ input });
} catch (e) {
if (e instanceof ZodError) {
return new Response("Invalid request body", { status: 400 });
}
return new Response("Internal Server Error", { status: 500 });
}Exhaustive Type Checking
Use never type for exhaustive switch statements:
switch (generation.context.operationNode.content.type) {
case "action":
// handle action
break;
case "textGeneration":
// handle text generation
break;
default: {
const _exhaustiveCheck: never = generation.context.operationNode.content.type;
throw new Error(`Unhandled type: ${_exhaustiveCheck}`);
}
}Fail Fast
Validate inputs early and throw with actionable messages:
if (!generation) {
throw new Error(`Generation(id: ${generationId}) is not found`);
}
if (!config.vectorStoreQueryService) {
throw new Error("No vector store query service provided");
}Feature flags protect unreleased features, allowing safe merges to main and production deploys.
Step 1: Define the flag in apps/studio.giselles.ai/flags.ts
export const myNewFeatureFlag = flag<boolean>({
key: "my-new-feature",
async decide() {
if (process.env.NODE_ENV === "development") {
return takeLocalEnv("MY_NEW_FEATURE_FLAG");
}
const edgeConfig = await get(`flag__${this.key}`);
if (edgeConfig === undefined) {
return false;
}
return edgeConfig === true || edgeConfig === "true";
},
description: "Enable my new feature",
options: [
{ value: false, label: "disable" },
{ value: true, label: "Enable" },
],
defaultValue: false,
});Step 2: Use on server (Next.js server components, data loaders)
// apps/studio.giselles.ai/app/workspaces/[workspaceId]/data-loader.ts
const myNewFeature = await myNewFeatureFlag();
return {
// ...
featureFlags: {
// ...existing flags
myNewFeature,
},
};Step 3: Expose to React components
Add the flag to the FeatureFlagContextValue interface:
// packages/react/src/feature-flags/context.ts
export interface FeatureFlagContextValue {
// ...existing flags
myNewFeature: boolean;
}Add to WorkspaceProvider defaults:
// packages/react/src/workspace/provider.tsx
<FeatureFlagContext
value={{
// ...existing flags
myNewFeature: featureFlag?.myNewFeature ?? false,
}}
>Step 4: Use in React components
import { useFeatureFlag } from "@giselles-ai/react";
function MyComponent() {
const { myNewFeature } = useFeatureFlag();
if (!myNewFeature) {
return null; // or fallback UI
}
return <NewFeatureUI />;
}Local development: Set the environment variable (e.g., MY_NEW_FEATURE_FLAG=true) in .env.local.
Production: Configure via Vercel Edge Config with key flag__my-new-feature.
Keep “human intent” and session context in-repo for review without frequent merge conflicts by using a two-layer model:
CONTINUITY.md: a batched snapshot (low churn), updated occasionally..continuity/: per-branch ledgers (high churn), updated on every request / during work.
- Locate ledger (every user request):
- Determine current git branch name:
git rev-parse --abbrev-ref HEAD. - Sanitize branch by replacing
/with__.
- Determine current git branch name:
- Find ledger file:
- In
.continuity/, find files whose filename ends with-<sanitizedBranch>.md(suffix match). - If multiple match, pick the latest by lexicographically greatest datetime prefix
YYYYMMDD-HHMMSS.
- In
- Reuse / create:
- If one exists: read it first and update it as needed.
- If none exists: create
YYYYMMDD-HHMMSS-<sanitizedBranch>.mdinitialized from.continuity/template.mdand the current user request.
- Read both
CONTINUITY.mdand the current.continuity/branch ledger to understand context. - Write high-churn notes only to
.continuity/(what changed, why, tradeoffs, open questions, working set). - Periodically batch-summarize
.continuity/*intoCONTINUITY.md(“as of ”).
functions.update_planis for short-term execution scaffolding while you work (a small 3–7 step plan with pending/in_progress/completed).CONTINUITY.mdis a batched summary; per-branch ledgers live in.continuity/.- Keep them consistent: summarize
.continuity/intoCONTINUITY.mdperiodically (not every micro-step).
- Begin with a brief “Ledger Snapshot” based on the current per-branch ledger (Goal + Now/Next + Open Questions). Print the full ledger only when it materially changes or when the user asks.
- Goal (incl. success criteria):
- Constraints/Assumptions:
- Key decisions:
- State:
- Done:
- Now:
- Next:
- Open questions (UNCONFIRMED if needed):
- Working set (files/ids/commands):
Source code for dependencies is available in opensrc/ for deeper understanding of implementation details.
See opensrc/sources.json for the list of available packages and their versions.
Use this source code when you need to understand how a package works internally, not just its types/interface.
To fetch source code for a package or repository you need to understand, run:
npx opensrc <package> # npm package (e.g., npx opensrc zod)
npx opensrc pypi:<package> # Python package (e.g., npx opensrc pypi:requests)
npx opensrc crates:<package> # Rust crate (e.g., npx opensrc crates:serde)
npx opensrc <owner>/<repo> # GitHub repo (e.g., npx opensrc vercel/ai)