effect-best-practices
Skill1k starsChanged 26 days agoBSD-3-ClauseView on GitHub
Enforces Effect-TS patterns for services, errors, layers, atoms, and Effect.pipe composition. Use when writing Effect.Service, Schema.TaggedError, Layer, effect-atom, Effect.fn/`.pipe`, or `yield*` pipelines.
- Reads credentials
5 other projects use a version of this, 5 of them edited; 1 new project in the last 4 weeks, against 0 the 4 before.
Adopted by
Surging: 1 new project took it in the last 4 weeks, none the 4 weeks before.thin data
Most recently taken up by
- Makisuo/skillsedited, first seen Oct 16, 2026
- millionco/expectedited, first seen Oct 8, 2026
- niieani/caaraedited, first seen Jul 5, 2026
- jliocsar/parleyedited, first seen May 28, 2026
- betalyra/effect-skillsedited, first seen Apr 13, 2026
Who's talking about forcedotcom/salesforcedx-vscode
- prettier/prettier-vscode“Couldn't resolve parser "json-stringify"”GitHub · Jun 5, 2018 · 23 reactions
- Stack Overflow“VSCode / Salesforce - Deploy Source to Org very slow to complete, although actual deployment is fast?”Stack Overflow · Jun 26, 2023 · 4 votes
- xixiaofinland/sf.nvim“Debugging functionality”GitHub · Oct 1, 2024 · 3 reactions
- EclipseFdn/open-vsx.org“Claiming namespace salesforce”GitHub · May 3, 2023 · 2 reactions
- forcedotcom/salesforcedx-apex“fix: set correct exit codes on failure”GitHub · Apr 23, 2021 · 2 reactions
The biggest 5 of 52 mentions linking to the repository that our crawl found. Each line is their own headline.
See all 52
Talked about on GitHub and Stack Overflow (52 times).
On GitHub · 50
- Jun 5, 2018Couldn't resolve parser "json-stringify" — prettier/prettier-vscode23 reactions · 6 comments
- Oct 1, 2024Debugging functionality — xixiaofinland/sf.nvim3 reactions · 14 comments
- May 3, 2023Claiming namespace salesforce — EclipseFdn/open-vsx.org2 reactions · 21 comments
- Apr 23, 2021fix: set correct exit codes on failure — forcedotcom/salesforcedx-apex2 reactions
- Sep 11, 2024E2E tests fail in VSCode version 1.93.0 with "target window already closed" when running "Developer: Reload Window" — webdriverio-community/wdio-vscode-service1 reaction · 11 comments
45 more on GitHub
- Sep 30, 2020Trigger completions on specific characters — forcedotcom/lightning-language-server1 reaction · 2 comments
- Jun 25, 2019Source commands do not display error information — forcedotcom/cli1 reaction · 34 comments
- Aug 27, 2018Inform extensions that call 'vscode.openFolder' with file URIs created with URI.parse — microsoft/vscode1 reaction
- Aug 3, 2017Show externalised strings in extension editor contributions tab — microsoft/vscode1 reaction · 8 comments
- Nov 6, 2026build(deps-dev): bump the development-dependencies group across 1 directory with 5 updates — certinia/debug-log-analyzer0 reactions
- Nov 5, 2026claude-code-plugins.github.forcedotcom.salesforcedx-vscode: 68.8.0 → 68.10.0 — sudosubin/agents.nix0 reactions
- Nov 5, 2026build(deps-dev): bump the development-dependencies group across 1 directory with 4 updates — certinia/debug-log-analyzer0 reactions · 1 comment
- Nov 4, 2026agent-skills.github.forcedotcom.salesforcedx-vscode: 68.8.0 → 68.10.0 — sudosubin/agents.nix0 reactions
- Nov 4, 2026build(deps-dev): bump the development-dependencies group across 1 directory with 2 updates — certinia/debug-log-analyzer0 reactions · 1 comment
- Oct 29, 2026claude-code-plugins.github.forcedotcom.salesforcedx-vscode: 68.6.1 → 68.8.0 — sudosubin/agents.nix0 reactions
- Oct 29, 2026agent-skills.github.forcedotcom.salesforcedx-vscode: 68.6.1 → 68.8.0 — sudosubin/agents.nix0 reactions
- Oct 27, 2026agent-skills.github.forcedotcom.salesforcedx-vscode: 68.6.0 → 68.6.1 — sudosubin/agents.nix0 reactions
- Oct 27, 2026claude-code-plugins.github.forcedotcom.salesforcedx-vscode: 68.6.0 → 68.6.1 — sudosubin/agents.nix0 reactions
- Oct 27, 2026build(deps-dev): bump @salesforce/playwright-vscode-ext from 1.7.0 to 1.7.1 in the development-dependencies group across 1 directory — certinia/debug-log-analyzer0 reactions · 1 comment
- Oct 26, 2026build(deps-dev): bump @salesforce/playwright-vscode-ext from 1.7.0 to 1.7.1 in the development-dependencies group across 1 directory — certinia/debug-log-analyzer0 reactions · 2 comments
- Oct 22, 2026build(deps-dev): bump the development-dependencies group across 1 directory with 10 updates — certinia/debug-log-analyzer0 reactions · 1 comment
- Oct 21, 2026claude-code-plugins.github.forcedotcom.salesforcedx-vscode: 68.4.0 → 68.6.0 — sudosubin/agents.nix0 reactions
- Oct 21, 2026agent-skills.github.forcedotcom.salesforcedx-vscode: 68.4.0 → 68.6.0 — sudosubin/agents.nix0 reactions
- Oct 21, 2026build(deps-dev): bump the development-dependencies group across 1 directory with 8 updates — certinia/debug-log-analyzer0 reactions · 1 comment
- Oct 19, 2026build(deps-dev): bump the development-dependencies group across 1 directory with 7 updates — certinia/debug-log-analyzer0 reactions · 1 comment
- Oct 16, 2026claude-code-plugins.github.forcedotcom.salesforcedx-vscode: 68.2.0 → 68.4.0 — sudosubin/agents.nix0 reactions
- Oct 16, 2026agent-skills.github.forcedotcom.salesforcedx-vscode: 68.2.0 → 68.4.0 — sudosubin/agents.nix0 reactions
- Oct 16, 2026build(deps-dev): bump @salesforce/vscode-services from 67.20.0 to 67.27.5 — forcedotcom/apex-language-support0 reactions · 2 comments
- Oct 16, 2026feat(git): replace isomorphic-git with Effect Git - W-22711959 — forcedotcom/source-tracking0 reactions · 3 comments
- Oct 16, 2026build(deps-dev): bump the development-dependencies group across 1 directory with 4 updates — certinia/debug-log-analyzer0 reactions · 1 comment
- Oct 14, 2026build(deps-dev): bump the development-dependencies group across 1 directory with 2 updates — certinia/debug-log-analyzer0 reactions · 1 comment
- Oct 14, 2026claude-code-plugins.github.forcedotcom.salesforcedx-vscode: 67.26.0 → 68.2.0 — sudosubin/agents.nix0 reactions
- Oct 13, 2026agent-skills.github.forcedotcom.salesforcedx-vscode: 67.26.0 → 68.2.0 — sudosubin/agents.nix0 reactions
- Oct 12, 2026build(deps-dev): bump @salesforce/vscode-services from 67.26.0 to 67.27.1 in the development-dependencies group — certinia/debug-log-analyzer0 reactions · 1 comment
- Oct 4, 2026claude-code-plugins.github.forcedotcom.salesforcedx-vscode: init at 67.26.0 — sudosubin/agents.nix0 reactions
- Oct 3, 2026agent-skills.github.forcedotcom.salesforcedx-vscode: 67.20.0 → 67.26.0 — sudosubin/agents.nix0 reactions
- Sep 27, 2026agent-skills.github.forcedotcom.salesforcedx-vscode: init at 67.20.0 — sudosubin/agents.nix0 reactions
- Sep 25, 2026build(deps-dev): bump @salesforce/vscode-services from 67.17.18 to 67.20.0 — forcedotcom/apex-language-support0 reactions · 1 comment
- Sep 24, 2026build(deps-dev): bump the development-dependencies group across 1 directory with 16 updates — certinia/debug-log-analyzer0 reactions
- Sep 18, 2026build(deps-dev): bump @salesforce/vscode-services from 67.15.0 to 67.17.4 — forcedotcom/apex-language-support0 reactions · 1 comment
- Sep 17, 2026build(deps-dev): bump the development-dependencies group across 1 directory with 14 updates — certinia/debug-log-analyzer0 reactions · 1 comment
- Sep 11, 2026build(deps-dev): bump the development-dependencies group across 1 directory with 9 updates — certinia/debug-log-analyzer0 reactions · 1 comment
- Aug 28, 2026build(deps-dev): bump @salesforce/vscode-services from 67.10.0 to 67.13.3 — forcedotcom/apex-language-support0 reactions
- Aug 24, 2026Use Org Metadata Catalog for find-missing-artifact org lookups - W-23971749 — forcedotcom/apex-language-support0 reactions · 3 comments
- Aug 21, 2026build(deps-dev): bump @salesforce/vscode-services from 67.7.1 to 67.10.0 — forcedotcom/apex-language-support0 reactions
- Aug 19, 2026feat: export stdValueSetRegistry.json from main index.ts to be called properly outside this repo - W-23802414 — forcedotcom/source-deploy-retrieve0 reactions
- Jul 31, 2026build(deps-dev): bump @salesforce/vscode-services from 67.6.0 to 67.7.1 — forcedotcom/apex-language-support0 reactions
- Jul 24, 2026build(deps): bump @salesforce/vscode-i18n from 66.7.1 to 66.8.0 — forcedotcom/apex-language-support0 reactions
- Jul 8, 2026Support new SFDX: Create OpenAPI Document from This Class (Beta) — RoseSilverSoftware/IlluminatedCloud0 reactions
- Jul 8, 2026Agentforce for Developers inline autocompletion not working (temporary Salesforce API regression) — RoseSilverSoftware/IlluminatedCloud0 reactions
On Stack Overflow · 2
Other versions5 projects use a version of this.
Word for word · 0
None found.
Edited · 5
- millionco/expect★ 3.6k48% same
- Makisuo/skills★ 7938% same
- betalyra/effect-skills★ 2036% same
- niieani/caara★ 438% same
- jliocsar/parley★ 040% same
Wrote their own version · 0
None found.
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository46 other files it gives its agents.
- AGENTS.mdAGENTS.md
- .cursor/rules/doc-maintenance.mdcCursor rule
- .cursor/rules/verification.mdcCursor rule
- .cursor/rules/wireit.mdcCursor rule
- vscode-extension-launchSkill
- announce-prereleaseSkill
- backlog-groomingSkill
- changelogSkill
- command-uiSkill
- conciseSkill
- core-extension-apiSkill
- dependabot-alertsSkill
- drivable-vscodeSkill
- external-consumersSkill
- feature-branchSkill
- grill-meSkill
- gus-cliSkill
- i18n-messagesSkill
- icons-srcSkill
- lit-componentsSkill
- lit-webview-architectureSkill
- lit-webview-testingSkill
- lsp-custom-requestsSkill
- merge-conflictsSkill
- packageJsonSkill
- patch-releaseSkill
- pathsSkill
- playwright-e2eSkill
- pr-draftSkill
- query-app-insightsSkill
- releaseSkill
- services-extension-consumptionSkill
- shipped-issuesSkill
- span-file-exportSkill
- thermo-nuclear-code-quality-reviewSkill
- ts1261-filename-casingSkill
- ts4023-effect-errorsSkill
- typescriptSkill
- verificationSkill
- vscode-elements-webviewsSkill
- vscode-window-messagesSkill
- web-console-local-tracesSkill
- wireitSkill
- work-item-sequencingSkill
- changelog-judgmentSkill
- manual-test-plan-judgmentSkill
The file
We split this skill into the rules it gives an agent
5 rules. 0 are common, 0 are distinctive, and 5 are very specific to this project.
0
Common
Most of the 5 edited versions we read keep these.
0
Distinctive
This author's own choices. Worth a look.
5
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 23
After a batch of edits, run --project tsconfig.json for the affected package to catch cross-file issues.
- Very specific: line 331
Never use
Option.getOrThrow: Always handle both cases explicitly: - Very specific: line 411
Keep Object.groupBy only when the Partial is load-bearing — e.g.
- Very specific: line 459
Use Predicate.not() for filter negations.
- Very specific: line 476
Keep ! where the predicate isn't element-level (!isCollectionType(decl.type)) unless a named element predicate already exists or is worth …
“Common” is measured: kept by at least 6 in 10 of the 5 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
For diff/plan review against these patterns, invoke the effect-advocate subagent (.claude/agents/effect-advocate.md).
Effect LS diagnostics (agent usage)
Cursor's read_lints does not surface Effect Language Server diagnostics. Use the CLI:
npx effect-language-service diagnostics --file <path>
# or whole project:
npx effect-language-service diagnostics --project tsconfig.json- The PostToolUse
verify-on-edit.shhook auto-runs--file <edited>on every.tsEdit/Write and surfaces output asfollowup_message. Address what it reports. - Address warnings AND messages, not just errors.
references/diagnostics-findings.mdmaps each common finding to its fix;config/effect-diagnostics.jsonenforcedRulesis the build gate. - Enforcing a rule takes two edits, not just an
enforcedRulesentry — seereferences/diagnostics-findings.md. - After a batch of edits, run
--project tsconfig.jsonfor the affected package to catch cross-file issues. effect-language-service quickfixesshows proposed code changes.
Quick Reference: Critical Rules
| Category | DO | DON'T |
| Services | Effect.Service with accessors: true | Context.Tag for business logic |
| Dependencies | dependencies: [Dep.Default] in service | Manual Layer.provide at usage sites |
| Errors | Schema.TaggedError with message field | Plain classes or generic Error |
| Error Specificity | Split tags when catch arms or field shapes differ; telemetry dimensions are fields on one tag | Extra tags that all print message / fire the same span |
| Error Handling | catchTag/catchTags; catch only when needed | catchAll; swallowing; catching "just in case" |
| IDs | Salesforce record/org: SalesforceId/OrgId (core/schemas/salesforceId.ts). DefaultOrgInfoSchema.orgId/devHubOrgId: Schema.optional(OrgId) like cliId. Else Schema.UUID.pipe(Schema.brand("@App/EntityId")) | Plain string; getAuthInfoFields().orgId ad hoc; optionalWith as Option on DefaultOrgInfo |
| Functions | Effect.fn over Effect.gen; .gen only for shared pipes | Anonymous generators; nested Effect.gen to attach recovery; .gen for business logic |
| Composition | .pipe; local/inline-single-use-yield-binding for yielded values; local/no-standalone-pipeable-value for Effect/Stream/Schema values. Details: references/composition-style.md | — |
| Params vs deps | Params = runtime data; dependencies = yield from context | Passing Ref/PubSub/service as params |
| Naming | FooCommand for commands, domain names for helpers | FooEffect suffix (redundant; TS/Effect.fn already convey type) |
| Logging | Effect.log with structured data | console.log |
| Config | Config.* with validation | process.env directly (except build-time vars like ESBUILD_*) |
| Time values | Duration.seconds(30), Duration.millis(5000); params as Duration.DurationInput. Enforced by local/no-raw-duration for a numeric arg/prop in Duration / DurationInput position only. bigint, template strings, number params, and TIMEOUT_MS constants stay outside the rule. | Numeric milliseconds as number params or TIMEOUT_MS = 30_000 constants |
| Options | Option.match with both cases | Option.getOrThrow |
| Nullability | Option<T> in domain types | null/undefined |
| Atoms | Atom.make outside components | Creating atoms inside render |
| Atom State | Atom.keepAlive for global state | Forgetting keepAlive for persistent state |
| Atom Updates | useAtomSet in React components | Atom.update imperatively from React |
| Atom Cleanup | get.addFinalizer() for side effects | Missing cleanup for event listeners |
| Resource Cleanup | Scoped service/layer + Effect.addFinalizer | Returning dispose; delegating Effect-owned resources to callers |
| Atom Results | Result.builder with onErrorTag | Ignoring loading/error states |
| Grouping | Arr.groupBy (effect/Array) | Object.groupBy, whose Partial<Record> forces a filter |
Service Definition Pattern
Always use Effect.Service for business logic services. This provides automatic accessors, built-in Default layer, and proper dependency declaration.
import { Effect } from 'effect';
export class UserService extends Effect.Service<UserService>()('UserService', {
accessors: true,
dependencies: [UserRepo.Default, CacheService.Default],
effect: Effect.gen(function* () {
const repo = yield* UserRepo;
const cache = yield* CacheService;
const findById = Effect.fn('UserService.findById')(function* (id: UserId) {
const cached = yield* cache.get(id);
if (Option.isSome(cached)) return cached.value;
const user = yield* repo.findById(id);
yield* cache.set(id, user);
return user;
});
const create = Effect.fn('UserService.create')(function* (data: CreateUserInput) {
const user = yield* repo.create(data);
yield* Effect.log('User created', { userId: user.id });
return user;
});
return { findById, create };
})
}) {}
// Usage - dependencies are already wired
const program = UserService.findById(userId);
// At app root
const MainLive = Layer.mergeAll(UserService.Default, OtherService.Default);When Context.Tag is acceptable:
- Infrastructure with runtime injection (Cloudflare KV, worker bindings)
- Factory patterns where resources are provided externally
- Interfaces with caller-provided implementations — no single canonical one to bundle as
.Default(e.g.SoqlBuilderService, implemented once by the VS Code host and once by a test fake); seereferences/service-patterns.md
Params vs Dependencies
- Params = runtime data per call (IDs, user input, per-invocation config)
- Dependencies = shared infrastructure (Ref, PubSub, SubscriptionRef, services) — provide via layer, yield inside the effect
- Build Ref/PubSub/etc in the layer (e.g.
buildAllServicesLayer); consumers yield them, don't receive as params
// WRONG - passing shared infra as params
const createStatusBar = (pubsub: PubSub.PubSub<void>, stateRef: SubscriptionRef.SubscriptionRef<State>) =>
Effect.gen(...)
// Caller must create and pass; wiring scattered at call sites
// CORRECT - yield inside, build in layer
const PubSubTag = Context.GenericTag<PubSub.PubSub<void>>("PubSub")
const createStatusBar = Effect.gen(function* () {
const pubsub = yield* PubSubTag
const stateRef = yield* StateRefTag
// ...
})
// Layer: Layer.effect(PubSubTag, PubSub.sliding<void>(1))See references/service-patterns.md for detailed patterns.
Error Definition Pattern
Always use Schema.TaggedError for errors. This makes them serializable (required for RPC) and provides consistent structure.
import { Schema } from 'effect';
import { HttpApiSchema } from '@effect/platform';
export class UserNotFoundError extends Schema.TaggedError<UserNotFoundError>()(
'UserNotFoundError',
{
userId: UserId,
message: Schema.String
},
HttpApiSchema.annotations({ status: 404 })
) {}
export class UserCreateError extends Schema.TaggedError<UserCreateError>()(
'UserCreateError',
{
message: Schema.String,
cause: Schema.optional(Schema.String)
},
HttpApiSchema.annotations({ status: 400 })
) {}Error handling - use catchTag/catchTags:
// CORRECT - preserves type information
yield *
repo.findById(id).pipe(
Effect.catchTag('DatabaseError', err =>
Effect.fail(new UserNotFoundError({ userId: id, message: 'Lookup failed' }))
),
Effect.catchTag('ConnectionError', err =>
Effect.fail(new ServiceUnavailableError({ message: 'Database unreachable' }))
)
);
// CORRECT - multiple tags at once
yield *
effect.pipe(
Effect.catchTags({
DatabaseError: err => Effect.fail(new UserNotFoundError({ userId: id, message: err.message })),
ValidationError: err => Effect.fail(new InvalidEmailError({ email: input.email, message: err.message }))
})
);When to Catch (and When Not To)
Most errors surface to the user (message/toast at runtime). Only catch when:
- Genuinely ignore – accept failure and continue (e.g. optional pre-create)
- Better message – default vague; map to clearer domain error
Expected skip (missing optional plugin, incompatible version): write nls on the success path (guard / Match branch). fail+catchTag is for unexpected recovery — a handler that isn't "print this string."
Catch sparingly. No catchAll or "swallow to be safe." Use catchTag/catchTags; log or fail with improved error.
Prefer Explicit Over Generic Errors
Split tags when catch arms or field shapes differ — e.g. message+cause vs message+cause+setting. Same catch work (print message, one span) → one tag; telemetry dimensions are fields, not tags. nls variance lives in message. Frontend/RPC still splits when the UI branches (UserNotFoundError vs ChannelNotFoundError). See references/error-patterns.md.
Accumulating Errors Across a Collection
To continue past failures instead of short-circuiting on the first, don't hand-roll Either + catchTag + a re-loop. Use Effect.partition (both buckets), Effect.validateAll (all-or-nothing), or Effect.validateFirst. These recover the typed error channel per item but do NOT capture interruption — so a Cancel still aborts the whole loop.
See references/error-patterns.md for the accumulation/interruption nuance, error remapping, and retry patterns.
Schema & Branded Types Pattern
Brand all entity IDs for type safety across service boundaries.
This repo — Salesforce record/org ids are not UUIDs. Use SalesforceId/OrgId and orgIdFrom/orgIdFromConnection (getFields() / Connection; Option; references/schema-patterns.md). Other AuthFields: authFieldsFrom/authFieldsFromConnection. DefaultOrgInfoSchema.orgId/devHubOrgId: Schema.optional(OrgId) like cliId — not Option.
import { Schema } from 'effect';
// Entity IDs - always branded
export const UserId = Schema.UUID.pipe(Schema.brand('@App/UserId'));
export type UserId = Schema.Schema.Type<typeof UserId>;
export const TenantId = Schema.UUID.pipe(Schema.brand('@App/TenantId'));
export type TenantId = Schema.Schema.Type<typeof TenantId>;
// Domain types - use Schema.Struct
export const User = Schema.Struct({
id: UserId,
email: Schema.String,
name: Schema.String,
tenantId: TenantId,
createdAt: Schema.DateTimeUtc
});
export type User = Schema.Schema.Type<typeof User>;
// Input types for mutations
export const CreateUserInput = Schema.Struct({
email: Schema.String.pipe(Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)),
name: Schema.String.pipe(Schema.minLength(1)),
tenantId: TenantId
});
export type CreateUserInput = Schema.Schema.Type<typeof CreateUserInput>;When NOT to brand:
- Simple strings that don't cross service boundaries (URLs, file paths)
- Primitive config values
See references/schema-patterns.md for transforms and advanced patterns.
Function Pattern: Prefer Effect.fn over Effect.gen
Prefer Effect.fn for effectful code. Provides automatic tracing with proper span names. Span name required; enforced by local/require-effect-fn-span-name. Effect.tap / *SuccessNotification middleware before catch middleware: local/effect-fn-catch-middleware-last (later catch and non-success guards stay valid). Details: services-extension-consumption Success handling.
Use Effect.gen only when you need a shared effect with common .pipe attached so multiple consumers don't each pipe the same things — e.g. provided dependencies, common error handlers, retries. (Less common with Runtimes.) Service definition bodies are a valid use (shared wiring).
// CORRECT - Effect.fn with descriptive name
const findById = Effect.fn('UserService.findById')(function* (id: UserId) {
yield* Effect.annotateCurrentSpan('userId', id);
return yield* repo.findById(id);
});
// CORRECT - Effect.fn with multiple parameters
const transfer = Effect.fn('AccountService.transfer')(function* (fromId: AccountId, toId: AccountId, amount: number) {
yield* Effect.annotateCurrentSpan('fromId', fromId);
yield* Effect.annotateCurrentSpan('toId', toId);
yield* Effect.annotateCurrentSpan('amount', amount);
// ...
});
// WRONG - params on wrapper arrow, generator has none (closure capture)
// Enforced by local/no-effect-fn-wrapper
const findByIdBad = (id: UserId) =>
Effect.fn('UserService.findById')(function* () {
yield* repo.findById(id); // id from closure
});
// WRONG - Effect.fn invoked immediately (config-enforced effectFnIife). Effect.fn builds a reusable
// function; for one-shot use write Effect.gen and keep the span with a piped withSpan.
const opened = Effect.fn('FsService.open')(function* () {
yield* fs.showTextDocument(uri);
})();
// CORRECT
const openedOk = Effect.gen(function* () {
yield* fs.showTextDocument(uri);
}).pipe(Effect.withSpan('FsService.open'));
// Naming: Don't append Effect. For commands use FooCommand; for helpers/lifecycle use domain names.
// WRONG: logGetEffect, executeAnonymousDocumentEffect, activateEffect
// CORRECT: logGetCommand, executeAnonymousCommand, executeAnonymous (helper), activation (lifecycle)See references/composition-style.md: .pipe, local/inline-single-use-yield-binding for single-use yielded values, terminal runner, point-free safety, Match dispatch, guard clauses, recovery on a subsequence.
Layer Composition
Declare dependencies in the service, not at usage sites:
// CORRECT - dependencies in service definition
export class OrderService extends Effect.Service<OrderService>()('OrderService', {
accessors: true,
dependencies: [UserService.Default, ProductService.Default, PaymentService.Default],
effect: Effect.gen(function* () {
const users = yield* UserService;
const products = yield* ProductService;
const payments = yield* PaymentService;
// ...
})
}) {}
// At app root - simple merge
const AppLive = Layer.mergeAll(
OrderService.Default,
// Infrastructure layers (intentionally not in dependencies)
DatabaseLive,
RedisLive
);See references/layer-patterns.md for testing layers and config-dependent layers.
Effect-Owned Resources
Resources created inside an Effect service/layer belong to its scope. Prefer scoped plus
Effect.addFinalizer or Effect.acquireRelease; don't expose dispose or delegate cleanup to a host lifecycle.
export class StatusService extends Effect.Service<StatusService>()('StatusService', {
accessors: true,
scoped: Effect.gen(function* () {
const item = vscode.window.createStatusBarItem();
yield* Effect.addFinalizer(() => Effect.sync(() => item.dispose()));
return { show: Effect.sync(() => item.show()) };
})
}) {}The layer owner must close its scope. A ManagedRuntime owns its layers' scopes; dispose it during extension
deactivation. Use context.subscriptions only for resources created outside Effect ownership.
Option Handling
Never use Option.getOrThrow. Always handle both cases explicitly:
// CORRECT - explicit handling
yield *
Option.match(maybeUser, {
onNone: () => Effect.fail(new UserNotFoundError({ userId, message: 'Not found' })),
onSome: user => Effect.succeed(user)
});
// CORRECT - with getOrElse for defaults
const name = Option.getOrElse(maybeName, () => 'Anonymous');
// CORRECT - Option.map for transformations
const upperName = Option.map(maybeName, n => n.toUpperCase());Effect Atom (Frontend State)
Reactive React state via @effect-atom/atom-react. Define atoms OUTSIDE components; keepAlive for state that must persist; useAtomSet to write; Result.builder to render effectful results; get.addFinalizer to clean up listeners.
import { Atom, Result, useAtomValue, useAtomSet } from '@effect-atom/atom-react';
const countAtom = Atom.make(0); // outside the component
const prefsAtom = Atom.make({ theme: 'dark' }).pipe(Atom.keepAlive); // persistent
function Counter() {
const count = useAtomValue(countAtom);
const setCount = useAtomSet(countAtom);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
// Effectful atom → Result; handle loading/error/success
function UserProfile() {
return Result.builder(useAtomValue(userAtom))
.onInitial(() => <div>Loading...</div>)
.onErrorTag('NotFoundError', () => <div>User not found</div>)
.onError(error => <div>Error: {error.message}</div>)
.onSuccess(user => <div>Hello, {user.name}</div>)
.render();
}SubscriptionRef
SubscriptionRef<A> is a mutable ref whose .changes stream always emits the current value as element 0, then all future mutations.
Implemented as (from effect/src/internal/subscriptionRef.ts):
stream.concat(stream.make(currentValue), stream.fromPubSub(pubsub))The Ref.get + pubsub subscription happen atomically under a semaphore — no events are missed.
// WRONG — prepended get is always redundant
Stream.concat(Stream.fromEffect(SubscriptionRef.get(ref)), ref.changes)
Stream.concat(Stream.make(yield* SubscriptionRef.get(ref)), ref.changes)
Stream.merge(Stream.fromEffect(SubscriptionRef.get(ref)), ref.changes)
// CORRECT — .changes already provides the snapshot
ref.changes.pipe(...)To skip the initial snapshot (e.g. avoid a spurious refresh on activation), use Stream.drop(1).
Grouping
Object.groupBy is typed Partial<Record<K, V[]>>, so every consumer filters or defaults the undefined. Arr.groupBy returns Record<K, NonEmptyArray<V>> — total, so Object.entries needs no guard.
import * as Arr from 'effect/Array';
const grouped = Arr.groupBy(ids, id => id.slice(0, 3)); // Record<string, NonEmptyArray<string>>
Object.entries(grouped).map(([prefix, group]) => query(prefix, group));
// vs Object.groupBy, where the same line needs:
// .filter((entry): entry is [string, string[]] => isNotUndefined(entry[1]))Keep Object.groupBy only when the Partial is load-bearing — e.g. destructuring absent keys with defaults, const { deploys = [], deleted = [] } = ....
Anti-Patterns (Forbidden)
The DON'T column above names each forbidden pattern. references/anti-patterns.md
has the complete list — each with rationale and the correct alternative.
Emptiness Guards: match the declared type
== null / != null are banned in effect packages (noNullCompare in eslint.config.mjs).
Pick the effect/Predicate guard by what the declared type actually admits — one guard per
union shape, no exceptions:
| declared type | guard |
T | undefined | isUndefined / isNotUndefined |
T | null | isNull / isNotNull |
T | null | undefined | isNullable / isNotNullable |
import * as Schema from 'effect/Schema';
import { isNotNull, isNotUndefined, isNullable, isUndefined } from 'effect/Predicate';
// T | undefined — the common case (Optional<T>, optional props, ?? sources)
if (isUndefined(maybeValue)) return;
Effect.filterOrFail(isNotUndefined, () => new NotFoundError({ message: '...' }));
// non-id string that must be non-blank — not `isNotUndefined && length > 0`
Effect.filterOrFail(Schema.is(Schema.NonEmptyString), () => new NotFoundError({ message: '...' }));
// AuthFields org id: `orgIdFrom` / `orgIdFromConnection` → `Option<OrgId>`; fail-if-missing via `Option.match`
// DefaultOrgInfo orgId: already `OrgId | undefined` (`Schema.optional(OrgId)` like cliId)
// T | null — e.g. RegExp.exec, JSON payload fields
const match = scriptRegex.exec(html);
if (isNotNull(match)) { … }
// T | null | undefined — e.g. `exclude?: vscode.GlobPattern | null` (optional AND nullable)
const arr = isNullable(exclude) ? undefined : [exclude];Runtime semantics: isUndefined is x === undefined, isNull is x === null, isNullable
is either (node_modules/effect/src/Predicate.ts:611,649,925). A wider guard than the type
needs still compiles — it just implies a case the type can't produce, so it reads as a lie
about the value. Note typeof x === 'object' && x !== null object-narrowing stays as-is;
that's a structural check, not an emptiness check.
Point-free Predicates in Filters
Use Predicate.not() for filter negations; combines point-free and readability.
import { not } from 'effect/Predicate';
// CORRECT — point-free negation
arr.filter(not(isFlowTest))
// AVOID — arrow wrapper around negation
arr.filter(x => !isFlowTest(x))Works with any predicate — built-in (isString, isError, isNotUndefined, isNotNullable) or
custom. Only the wrapped predicate can be receiver-sensitive: not(obj.method)
detaches the receiver, so verify the impl uses no this first (see
composition-style).
Keep ! where the predicate isn't element-level (!isCollectionType(decl.type))
unless a named element predicate already exists or is worth adding.
Imports: Prefer Deep Imports from @effect/platform
Barrel imports from @effect/platform bundle HttpApiSwagger (Swagger UI), which esbuild cannot tree-shake. This bloats web/desktop bundles by ~5.5MB per output and can trigger security scanner false positives (e.g., ClamAV signatures).
Always import the submodule as a namespace (the submodule is the namespace — it has no self-named export):
// CORRECT — deep namespace import, tree-shakes unused
import * as FetchHttpClient from '@effect/platform/FetchHttpClient';
// use: FetchHttpClient.layer
// WRONG — barrel import drags in HttpApiSwagger
import { FetchHttpClient } from '@effect/platform';Matches the repo's import * as Effect from 'effect/Effect' style. Applies to all @effect/* packages. Check import source before committing.
Observability
// Structured logging
yield * Effect.log('Processing order', { orderId, userId, amount });
// Metrics
const orderCounter = Metric.counter('orders_processed');
yield * Metric.increment(orderCounter);
// Config with validation
const config = Config.all({
port: Config.integer('PORT').pipe(Config.withDefault(3000)),
apiKey: Config.secret('API_KEY'),
maxRetries: Config.integer('MAX_RETRIES').pipe(
Config.validate({ message: 'Must be positive', validation: n => n > 0 })
)
});See references/observability-patterns.md for metrics and tracing patterns.
Reference Files
For detailed patterns, consult these reference files in the references/ directory:
composition-style.md-.pipe;local/inline-single-use-yield-bindingfor single-use yielded values; terminal runner; point-free safety; tap for side effects; Match dispatch; guard clauses; linear body as point-free pipe vs generator; recovery on a subsequence (pipe from the first Effect — not a nestedEffect.gen)service-patterns.md- Service definition, Effect.fn, Context.Tag exceptionserror-patterns.md- Schema.TaggedError, error remapping, retry patternsschema-patterns.md- Branded types, transforms, Schema.Classlayer-patterns.md- Dependency composition, testing layersanti-patterns.md- Complete list of forbidden patternsdiagnostics-findings.md- Effect LS finding → fix, per ruleobservability-patterns.md- Logging, metrics, config patterns
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

