Agent skill

Effect Best Practices

by forcedotcom in forcedotcom/salesforcedx-vscode

Enforces Effect-TS patterns for services, errors, layers, atoms, and Effect.pipe composition.

BSD-3-ClauseAuto-check passedSales & Support

Install Effect Best Practices

skills CLI
$ npx skills add forcedotcom/salesforcedx-vscode --skill effect-best-practices -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install forcedotcom/salesforcedx-vscode effect-best-practices --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/forcedotcom/salesforcedx-vscode.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.claude/skills/effect-best-practices .claude/skills/effect-best-practices && rm -rf skills-src

Use ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.

Claude Code skills documentation · loads skills from .claude/skills/

Facts

Skill name
effect-best-practices
GitHub stars
1k
Token cost
~6.2k tokens
SKILL.md length
1,476 words
Files
9 (incl. references)
Skills in repo
37
Repo updated
First seen
Licence
BSD-3-Clause

At a glance

Enforces Effect-TS patterns for services, errors, layers, atoms, and Effect.pipe composition.

  • Writing Effect.Service
  • SKILL.md covers Effect LS diagnostics (agent…, Quick Reference: Critical Rules, Service Definition Pattern and Error Definition Pattern, plus 13 more sections
  • Calls npx; needs API_KEY
  • Schema.TaggedError

What it does

Effect Best Practices is an agent skill from forcedotcom/salesforcedx-vscode. 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.

Its SKILL.md is about 6.2k tokens, which your agent loads only when the skill is triggered. The skill folder holds 9 other files, including reference files (for example `references/anti-patterns.md`, `references/composition-style.md` and `references/diagnostics-findings.md`).

It sits in Sales & Support. It works with Salesforce. The repository describes itself as: Salesforce Extensions for VS Code. The licence is BSD-3-Clause.

When your agent uses it

  • Writing Effect.Service
  • Schema.TaggedError
  • Effect.fn/.pipe
  • Yield pipelines

Example prompts

  • “Use the effect-best-practices skill to enforce Effect-TS patterns for services, errors, layers, atoms, and Effect.pipe composition”
  • “/effect-best-practices”

Requirements

  • Node.js

What it can do on your machine

Read from SKILL.md and the folder at commit f7bb6fe. It shows what the files ask for, not the result of running them.

  • Tool permissions

    Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.

    From allowed-tools in the SKILL.md frontmatter.

  • Runs code

    Shell commands in SKILL.md call:

    • npx

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    No URLs in SKILL.md. Its commands use npx, which can reach the network depending on how they are called.

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names these keys or tokens, usually read from environment variables:

    • API_KEY

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

Effect Best Practices loads about 6.2k tokens when it runs, and up to ~29k if it reads all its reference files. Until then it costs about 58 tokens; SKILL.md has 1,476 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~58
When it runs · the whole SKILL.md, loaded when a task matches
~6.2k
With references · SKILL.md plus every file in references/, read only if the agent opens them
~29k

Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.

Safety

Auto-check passed

The automated check found no risky patterns in SKILL.md.

Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.

SKILL.md

The full file from forcedotcom/salesforcedx-vscode at commit f7bb6fe, republished under its BSD-3-Clause licence (© forcedotcom). 1,476 words, ~6,240 tokens.

Download SKILL.mdSave it as .claude/skills/effect-best-practices/SKILL.md (or your agent's skills folder). This skill also uses 8 other files; get the full folder from GitHub.
name
effect-best-practices
description
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.
review
always
version
1.6.1

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:

bash
npx effect-language-service diagnostics --file <path>
# or whole project:
npx effect-language-service diagnostics --project tsconfig.json
  • The PostToolUse verify-on-edit.sh hook auto-runs --file <edited> on every .ts Edit/Write and surfaces output as followup_message. Address what it reports.
  • Address warnings AND messages, not just errors. references/diagnostics-findings.md maps each common finding to its fix; config/effect-diagnostics.json enforcedRules is the build gate.
  • Enforcing a rule takes two edits, not just an enforcedRules entry — see references/diagnostics-findings.md.
  • After a batch of edits, run --project tsconfig.json for the affected package to catch cross-file issues.
  • effect-language-service quickfixes shows proposed code changes.

Quick Reference: Critical Rules

CategoryDODON'T
ServicesEffect.Service with accessors: trueContext.Tag for business logic
Dependenciesdependencies: [Dep.Default] in serviceManual Layer.provide at usage sites
ErrorsSchema.TaggedError with message fieldPlain classes or generic Error
Error SpecificitySplit tags when catch arms or field shapes differ; telemetry dimensions are fields on one tagExtra tags that all print message / fire the same span
Error HandlingcatchTag/catchTags; catch only when neededcatchAll; swallowing; catching "just in case"
IDsSalesforce 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
FunctionsEffect.fn over Effect.gen; .gen only for shared pipesAnonymous generators; nested Effect.gen to attach recovery; .gen for business logic
Composition.pipe; const only if read ≥2×. Details: references/composition-style.mdsingle-use const x = yield* then f(x)
Params vs depsParams = runtime data; dependencies = yield from contextPassing Ref/PubSub/service as params
NamingFooCommand for commands, domain names for helpersFooEffect suffix (redundant; TS/Effect.fn already convey type)
LoggingEffect.log with structured dataconsole.log
ConfigConfig.* with validationprocess.env directly (except build-time vars like ESBUILD_*)
Time valuesDuration.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
OptionsOption.match with both casesOption.getOrThrow
NullabilityOption<T> in domain typesnull/undefined
AtomsAtom.make outside componentsCreating atoms inside render
Atom StateAtom.keepAlive for global stateForgetting keepAlive for persistent state
Atom UpdatesuseAtomSet in React componentsAtom.update imperatively from React
Atom Cleanupget.addFinalizer() for side effectsMissing cleanup for event listeners
Resource CleanupScoped service/layer + Effect.addFinalizerReturning dispose; delegating Effect-owned resources to callers
Atom ResultsResult.builder with onErrorTagIgnoring loading/error states
GroupingArr.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.

typescript
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); see references/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
typescript
// 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.

typescript
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:

typescript
// 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.

typescript
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).

typescript
// 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, const only if read ≥2×, terminal runner, point-free safety, Match dispatch, guard clauses, recovery on a subsequence.

Show full SKILL.md (574 more words)Show less

Layer Composition

Declare dependencies in the service, not at usage sites:

typescript
// 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.

typescript
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:

typescript
// 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.

typescript
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):

ts
stream.concat(stream.make(currentValue), stream.fromPubSub(pubsub))

The Ref.get + pubsub subscription happen atomically under a semaphore — no events are missed.

typescript
// 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.

typescript
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 typeguard
T | undefinedisUndefined / isNotUndefined
T | nullisNull / isNotNull
T | null | undefinedisNullable / isNotNullable
typescript
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.

typescript
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):

typescript
// 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

typescript
// 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; const only if read ≥2×; 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 nested Effect.gen)
  • service-patterns.md - Service definition, Effect.fn, Context.Tag exceptions
  • error-patterns.md - Schema.TaggedError, error remapping, retry patterns
  • schema-patterns.md - Branded types, transforms, Schema.Class
  • layer-patterns.md - Dependency composition, testing layers
  • anti-patterns.md - Complete list of forbidden patterns
  • diagnostics-findings.md - Effect LS finding → fix, per rule
  • observability-patterns.md - Logging, metrics, config patterns

© forcedotcom, BSD-3-Clause. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

SKILL.md and 8 other files (references) in .claude/skills/effect-best-practices of forcedotcom/salesforcedx-vscode.

  • SKILL.md
  • references/anti-patterns.md
  • references/composition-style.md
  • references/diagnostics-findings.md
  • references/error-patterns.md
  • references/layer-patterns.md
  • references/observability-patterns.md
  • references/schema-patterns.md
  • references/service-patterns.md

Open the folder on GitHubat commit f7bb6fe

Compare with similar skills

Effect Best Practices next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.

Effect Best Practices compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
Effect Best Practices this skillforcedotcom/salesforcedx-vscode1k—~6.2kAutomated safety check: PassBSD-3-Clause
Soql Lib Query Builderbeyond-the-cloud-dev/soql-lib154—~4.3kAutomated safety check: PassMIT
Sf DatacloudJaganpro/sf-skills424—~2.7kAutomated safety check: PassMIT
Soql Lib Selectorbeyond-the-cloud-dev/soql-lib154—~2kAutomated safety check: PassMIT
Dev SetupPortwood-Global-Solutions/Portwood125—~1.1kAutomated safety check: PassApache-2.0
Sf FlowJaganpro/sf-skills424—~1.8kAutomated safety check: PassMIT

Similar skills

  • Soql Lib Query Builder

    beyond-the-cloud-dev/soql-lib

    Builds Salesforce SOQL queries using the SOQL Lib fluent builder API (SOQL.cls).

    154 GitHub stars~4.3k tokensUpdated 4 days ago
    Sales & SupportAuto-check passed
  • Sf Datacloud

    Jaganpro/sf-skills

    Salesforce Data Cloud product orchestrator for connect→prepare→harmonize→segment→act workflows.

    424 GitHub stars~2.7k tokensUpdated 5 mo ago
    Sales & SupportAuto-check passed
  • Soql Lib Selector

    beyond-the-cloud-dev/soql-lib

    Creates Salesforce Apex selector classes using the SOQL Lib selector pattern.

    154 GitHub stars~2k tokensUpdated 4 days ago
    Sales & SupportAuto-check passed
  • Dev Setup

    Portwood-Global-Solutions/Portwood

    Get from a fresh clone of Portwood to a working, fully-tested Salesforce org.

    125 GitHub stars~1.1k tokensUpdated 2 days ago
    Sales & SupportAuto-check passed
  • Sf Flow

    Jaganpro/sf-skills

    Creates and validates Salesforce Flows with 110-point scoring.

    424 GitHub stars~1.8k tokensUpdated 5 mo ago
    Sales & SupportAuto-check passed
  • Apply a Salesforce sandbox post-copy automation JSON config against a target org.

    1.1k GitHub stars~5.3k tokensUpdated 5 days ago
    Sales & SupportAuto-check: notes

More from forcedotcom/salesforcedx-vscode

All 37 skills in this repo
  • Command UI

    forcedotcom/salesforcedx-vscode

    Command palette, CodeLens, context menus, package.nls titles, and NotificationModeService.

    1k GitHub stars~1.4k tokensUpdated today
    Auto-check passed
  • Services Extension Consumption

    forcedotcom/salesforcedx-vscode

    Consume the salesforcedx-vscode-services extension API. An agent skill from forcedotcom/salesforcedx-vscode.

    1k GitHub stars~5k tokensUpdated today
    Auto-check passed
  • Changelog

    forcedotcom/salesforcedx-vscode

    Polish the automated CHANGELOG on develop before the next stable build.

    1k GitHub stars~3.3k tokensUpdated today
    Auto-check passed
  • Core Extension API

    forcedotcom/salesforcedx-vscode

    Public API exported by salesforcedx-vscode-core activate(). An agent skill from forcedotcom/salesforcedx-vscode.

    1k GitHub stars~842 tokensUpdated today
    Auto-check passed
  • Drivable Vscode

    forcedotcom/salesforcedx-vscode

    Operate a real VS Code instance through drivable-vscode. An agent skill from forcedotcom/salesforcedx-vscode.

    1k GitHub stars~1k tokensUpdated today
    Auto-check passed
  • External Consumers

    forcedotcom/salesforcedx-vscode

    Known external consumers of APIs from this monorepo's extensions.

    1k GitHub stars~1.8k tokensUpdated today
    Auto-check passed

Works with

Categories

Questions about Effect Best Practices

What does Effect Best Practices do?

Enforces Effect-TS patterns for services, errors, layers, atoms, and Effect.pipe composition. Effect Best Practices is an agent skill from forcedotcom/salesforcedx-vscode.pipe composition.

When should I use Effect Best Practices?

Effect Best Practices fits situations like: writing Effect.Service; schema.TaggedError; effect.fn/.pipe; yield pipelines.

How do I install Effect Best Practices in Claude Code?

Run `npx skills add forcedotcom/salesforcedx-vscode --skill effect-best-practices -a claude-code`. Or copy the skill folder (.claude/skills/effect-best-practices in forcedotcom/salesforcedx-vscode) into .claude/skills/effect-best-practices in your project. Claude Code loads it when a task matches its description.

How do I install Effect Best Practices in Codex?

Run `npx skills add forcedotcom/salesforcedx-vscode --skill effect-best-practices -a codex`. Or copy the skill folder (.claude/skills/effect-best-practices in forcedotcom/salesforcedx-vscode) into .agents/skills/effect-best-practices in your project. Codex loads it when a task matches its description.

Can I use Effect Best Practices in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add forcedotcom/salesforcedx-vscode --skill effect-best-practices -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/effect-best-practices, .gemini/skills/effect-best-practices, .github/skills/effect-best-practices and .opencode/skills/effect-best-practices in your project.

What does Effect Best Practices need to run?

Going by SKILL.md and its folder, Effect Best Practices needs the command-line tools its instructions call (npx) and credentials named API_KEY. Our summary lists: Node.js.

Does Effect Best Practices access the network?

SKILL.md contains no URLs. Its commands use npx, which can reach the network depending on how they are called. This is read from the text; nothing was executed.

Is Effect Best Practices safe to install?

Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.

What licence does Effect Best Practices use?

Effect Best Practices is published under the BSD-3-Clause licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does Effect Best Practices use?

About 6.2k tokens (SKILL.md is roughly 25k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full. Its references folder adds about 23k tokens, read only when the agent opens those files.

What are the alternatives to Effect Best Practices?

Skills that share tags, products or a category with Effect Best Practices: Soql Lib Query Builder (beyond-the-cloud-dev/soql-lib, 154 stars), Sf Datacloud (Jaganpro/sf-skills, 424 stars), Soql Lib Selector (beyond-the-cloud-dev/soql-lib, 154 stars) and Dev Setup (Portwood-Global-Solutions/Portwood, 125 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains Effect Best Practices?

forcedotcom (a GitHub organization) maintains it in forcedotcom/salesforcedx-vscode, which has 1,035 GitHub stars. The repository holds 37 skills in this directory. The repository was last updated on October 7, 2026.

Source: forcedotcom/salesforcedx-vscode on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.