agentsclimarketplace

Workflow gateway

Skill marzun9620/agent_skills/workflow/skills/workflow-gateway

Gateway (external API client) implementation guide. Define the Port as a Context.Tag and implement with Layer.effect in Infrastructure. Wrap errors in GatewayError and apply externalApiRetryPolicy. Triggers: gateway creation, external API, Capsule, TableCheck, HTTP client, external service integration, API call, webhook.From its SKILL.md

Install
npx -y skills add marzun9620/agent_skills --skill workflow-gateway

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

5.9 KB, ~1.6k tokens by cl100k_base, as published. Nobody here has run it

Gateway 実装手順

ADR-0004 / ADR-0006 準拠。外部サービス(Capsule CRM, TableCheck 等)への呼び出しを担当。 Repository と同じ Context.Tag + Layer.effect パターンだが、エラー型と retry policy が異なる。

Import ルール(重要)

全ての cross-directory import は barrel(index.js)経由 でなければならない(ADR-0004 §7):

// ✅ 正しい
import { GatewayError } from "~/usecase/ports/index.js";
import { AppConfig } from "~/packages/config/index.js";
import { externalApiRetryPolicy, withRetryLogging } from "~/packages/resilience/index.js";

// ❌ 禁止
import { AppConfig } from "~/packages/config/appConfig.js";
import { GatewayError } from "~/usecase/ports/errors.js";

Repository との違い

RepositoryGateway
エラー型RepositoryErrorGatewayError
RetrydbRetryPolicy (3回, 100ms)externalApiRetryPolicy (2回, 500ms)
I/ODrizzle ORM → PostgreSQLHTTP client → 外部 API
MapperDB row → Domain entityJSON response → Domain entity
テストReal DB (Testcontainers)Mock HTTP (msw 等) or contract test

1. Port 定義

ファイル: apps/datahub/src/usecase/ports/{service}Gateway.ts

import { Context, Effect } from "effect";
import { GatewayError } from "./errors.js";
import { Customer } from "~/domain/customer/index.js";

export class CapsuleCrmGateway extends Context.Tag("CapsuleCrmGateway")<
  CapsuleCrmGateway,
  {
    readonly searchParties: (query: string) => Effect.Effect<ReadonlyArray<Customer>, GatewayError>;
    readonly updateParty: (id: string, data: CustomerUpdate) => Effect.Effect<void, GatewayError>;
  }
>() {}
  • エラー型は GatewayErrorusecase/ports/errors.ts に既存)
  • RepositoryError ではなく GatewayError を使う

Port の barrel export: usecase/ports/index.ts に追加。

2. Infrastructure 実装

ファイル: apps/datahub/src/infrastructure/gateway/{service}/{service}GatewayLive.ts

import { Effect, Layer } from "effect";
import { CapsuleCrmGateway } from "~/usecase/ports/index.js";
import { GatewayError } from "~/usecase/ports/index.js";
import { AppConfig } from "~/packages/config/index.js";
import { withRetryLogging, externalApiRetryPolicy } from "~/packages/resilience/index.js";
import { toDomain } from "./mappers/partyMapper.js";

export const CapsuleCrmGatewayLive = Layer.effect(
  CapsuleCrmGateway,
  Effect.gen(function* () {
    const config = yield* AppConfig;
    const apiKey = config.capsuleApiKey;

    return {
      searchParties: (query) =>
        withRetryLogging(
          Effect.tryPromise({
            try: () =>
              fetch(`https://api.capsulecrm.com/api/v2/parties/search?q=${encodeURIComponent(query)}`, {
                headers: { Authorization: `Bearer ${apiKey}` },
              }).then((r) => r.json()),
            catch: (cause) => new GatewayError({ cause }),
          }).pipe(
            Effect.map((response) => response.parties.map(toDomain)),
            Effect.withLogSpan("CapsuleCrmGateway.searchParties"),
          ),
          externalApiRetryPolicy,
          "CapsuleCrmGateway.searchParties",
        ),
    };
  }),
);

重要な違い(Repository との比較):

  • Effect.tryPromise の catch で GatewayErrorRepositoryError ではない)
  • externalApiRetryPolicy(2回, 500ms — dbRetryPolicy の3回, 100ms ではない)
  • AppConfig から API キーを取得
  • PII を含む外部レスポンスのログ出力禁止(ADR-0006)

3. Mapper 定義

ファイル: apps/datahub/src/infrastructure/gateway/{service}/mappers/{entity}Mapper.ts

import { Customer, CustomerId } from "~/domain/customer/index.js";

type CapsuleParty = {
  id: number;
  firstName: string;
  lastName: string;
  // ...
};

export const toDomain = (party: CapsuleParty): Customer =>
  new Customer({
    id: CustomerId.make(String(party.id)),
    name: `${party.firstName} ${party.lastName}`,
    // ...
  });
  • 外部 API のレスポンス型 → Domain Entity への変換
  • PII フィールドをログに出さないこと

4. DI 登録

ファイル: apps/datahub/src/di/appLayer.ts

InfraLayerLayer.mergeAllCapsuleCrmGatewayLive を追加。

5. ディレクトリ構造

infrastructure/gateway/
├── capsuleCrm/
│   ├── index.ts                    # barrel export
│   ├── capsuleCrmGatewayLive.ts    # Layer.effect 実装
│   └── mappers/
│       └── partyMapper.ts          # 外部 JSON → Domain
└── tableCheck/
    ├── index.ts
    ├── tableCheckGatewayLive.ts
    └── mappers/
        └── bookingMapper.ts

テスト踏襲ルール

  • Gateway のテストは外部 API をモックする(msw 等)
  • Usecase テストでは Layer.succeed(GatewayTag, { method: mockImpl }) で mock
  • Effect.either + table-driven + AAA パターンを踏襲
  • 外部 API のタイムアウト・レート制限エラーのケースも含める

チェックリスト

  • Port は Context.Tag で定義
  • エラーは GatewayErrorRepositoryError ではない)
  • externalApiRetryPolicy を適用
  • withRetryLogging でラップ
  • Effect.withLogSpan でスパン付与
  • PII を含むデータをログに出力していない(ADR-0006)
  • AppConfig から API キーを取得(ハードコード禁止)
  • DI 層(appLayer.ts)に登録済み
  • barrel export が存在

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 326,367. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.