Use when building or editing a Go backend that combines Encore.dev (infrastructure-as-code services, typed SQL, cron, pubsub, auth, secrets) with CloudWeEino (LLM orchestration — ChatModel, ReAct agent, tools, retriever, streaming). Produces Encore service scaffolding, an Eino pipeline hosted behind an `//encore:api`, a green `encore test`/`go test ./...`, and a code-gen-clean `encore.gen.go`. Grounded in Encore.go v1.57.x + Eino v0.9.x canonical bases; extends with the 8syncdev go-kit workflow and omp-native code-intelligence tooling.
Resources
1Install
npx skillscat add 8-sync-dev/8sync/encore-eino-go Install via the SkillsCat registry.
encore-eino-go — Encore.go + Eino LLM orchestration in one Go codebase
When to use
- You are starting or extending a Go service group where Encore hosts the infra (services, DB, cron, pubsub, auth, secrets) and an LLM pipeline (Eino ChatModel → ReAct agent → tools → streaming) runs inside an Encore API/service.
- User says any of: "Encore Go service", "add an
//encore:api", "encore.gen.go is stale", "wire Eino into Encore", "ReAct agent in Go", "stream tokens from an Encore endpoint", "encore cron / pubsub / secret / auth handler". - You need to verify a running Encore app boots, serves, and the Eino graph streams end-to-end.
When NOT to use
- Encore.ts (TypeScript variant) is the target — this skill is Go-first. Encore.ts shares the annotation model but a different SDK/runtime; defer to Encore.ts-specific guidance. (Noted only as a sibling, not migrated here.)
- Pure Go with no Encore and no LLM — use the language skill, not this one.
- Migrating an existing Encore.ts app to Go, or hand-rewriting generated
encore.gen.go.
Mental model: how the two layers compose
- Encore = infra boundary. A service is a Go package with
//encore:service+ an optionalinitService. APIs are annotatedfuncs (//encore:api). Encore generatesencore.gen.go(the typed client surface + types) — you never edit it. SQL is typed viaencore.dev/storage/sqldb; cron/pubsub/auth/secrets areencore.dev/*packages wired by annotation, not config files. - Eino = LLM boundary. Components (
model.ToolCallingChatModel,tool.BaseTool, retriever, embedder) are composed by Eino'sflow/agent/react(orcompose) into an agent. The agent is an ordinary Go object — you build it inside an Encore service and call it from an API handler. - The seam: Encore owns the request lifecycle (auth, DB, pubsub, HTTP/SSE); Eino owns the turn (
ctxin →*schema.Message/*schema.StreamReaderout). One Encore service package typically wraps one Eino agent build + aProcessTurn/ProcessTurnStreamentrypoint.
See references/base.md for the canonical concepts that break everything if wrong, and references/patterns.md for verified 8syncdev go-kit patterns.
Procedure
0. Ground before you touch code (code-intelligence first — mandatory)
Per the 8sync CLI canon (see
8syncdev/auto-work-cloudgo), structural queries replace blind grep/read. This is the single biggest token saver in a generated-code-heavy Encore repo.
- Index the Encore app once:
codegraph index .at the repo root (the dir holdingencore.app). - Map the generated API surface —
encore.gen.gois the source of truth for declared endpoints/types:codegraph queryfor symbols inencore.gen.go(cbm / serena below only if enabled via8sync harness cbm|serena on); ormcp__codebase_memory_mcp_search_graph/semantic_queryto find services and their callers.mcp__codebase_memory_mcp_get_architecturefor the service→service call graph (Encore services call each other via the generated client, so the graph is the dependency map).
- For a service you are about to edit:
mcp__serena_find_symbol/find_referencing_symbolsto get every caller of an API before changing its signature (signature change ⇒encore.gen.goregenerates ⇒ all callers must recompile). - Compress any tool dump > ~50 lines with
mcp__headroom_compressbefore reasoning over it.
1. Define / extend an Encore service
- A service = a package with
//encore:serviceand an optional bootstrap://encore:service type Service struct{} func initService() (*Service, error) { // load secrets, warm caches, bootstrap key pools — runs before first request return &Service{}, nil } - Add an API (the annotation is the route — no router registration):
Access modifiers://encore:api public method=POST path=/mypkg.DoThing func DoThing(ctx context.Context, req *ThingReq) (*ThingResp, error) { ... }public(anyone),auth(requires the auth handler),private(Encore-internal / cron / pubsub only). Userawfor fullhttp.ResponseWritercontrol (SSE, file streaming). - After any signature/annotation change, run
encore run(dev) orencore build— Encore regeneratesencore.gen.go. Never hand-editencore.gen.go. Confirm it is gitignored (it is regen-only).
2. Typed SQL + migrations
- Declare a named DB:
var db = sqldb.Named("my_db")(the name maps to an Encore infra DB; local dev uses Encore's built-in Postgres). - Migrations live in
<service>/migrations/<n>_name.up.sql(+ optional.down.sql), applied in order. Schema changes = new migration files, neverALTERin app code. - Typed queries:
db.QueryRow(ctx, "...", args...).Scan(...),db.Exec,db.Query(iteraterows.Next()). Seereferences/patterns.md§SQL.
3. Encore infra primitives (annotation-driven)
- Secret:
var secrets struct { FooKey string }→ set viaencore secret set --type prod,dev,pr,local FooKey. Readsecrets.FooKey; never log the value. - Cron:
cron.NewJob("id", cron.JobConfig{Every: 1*cron.Minute, Endpoint: MySweep})whereMySweepis a//encore:api privatefunc. - PubSub: publish =
pubsub.NewTopic[*Evt]("topic", pubsub.TopicConfig{DeliveryGuarantee: pubsub.AtLeastOnce})thentopic.Publish(ctx, ev). Consume =pubsub.NewSubscription(topic, "worker", pubsub.SubscriptionConfig[*Evt]{AckDeadline: ..., Handler: fn}). SetAckDeadline≥ your handler's worst-case latency or the broker will redeliver mid-run. - Auth: one
//encore:authhandlerfunc(ctx, *AuthParams) (auth.UID, *AuthData, error)reads headers/cookies; any//encore:api authendpoint is gated by it. Returnerrs.Unauthenticatedon failure (never a bare error — Encore treats other codes as a hard abort).
4. Build the Eino pipeline (LLM orchestration)
- ChatModel via the matching
eino-extcomponent, keyed off a resolved config + provider key:
Dispatch by provider API flavor (OpenAI-compatible base_url vs Gemini), not by vendor name. Cache constructed models per (provider,model,key).import ( openaimodel "github.com/cloudwego/eino-ext/components/model/openai" "github.com/cloudwego/eino/components/model" ) cm, err := openaimodel.NewChatModel(ctx, &openaimodel.ChatModelConfig{ APIKey: key, BaseURL: baseURL, Model: name, Timeout: 60*time.Second, }) - Tools from plain Go funcs:
utils.InferTool(name, desc, fn)→tool.BaseTool. Assemble a registrymap[string]tool.BaseTool. - ReAct agent from the ChatModel + toolset:
Setimport ( "github.com/cloudwego/eino/compose" "github.com/cloudwego/eino/flow/agent/react" ) agent, err := react.NewAgent(ctx, &react.AgentConfig{ ToolCallingModel: cm, ToolsConfig: compose.ToolsNodeConfig{Tools: toolset, ExecuteSequentially: true}, MaxStep: maxStep, })ExecuteSequentially: truewhen write-tools share mutable per-turn state (guards, idempotency stamps) — Eino's default runs one message's tool calls concurrently. - Stream:
sr, err := agent.Stream(ctx, msgs)returns*schema.StreamReader[*schema.Message]; relay chunks to the client. Non-streaming:agent.Generate(ctx, msgs). - Retriever/embedder for RAG: implement the Eino
Retriever/Embeddercomponent interface (or call a provider/embeddingsendpoint), store vectors in Postgres (pgvector), filterWHERE embedding <=> $1 < k.
5. Host the Eino pipeline as an Encore service
- Put the Eino build (model factory, tool registry, agent) in one service package; expose
ProcessTurn/ProcessTurnStreamas package-level funcs. - An
//encore:api raw(orprivate+ arawSSE shim) endpoint receives the request, builds/looks-up the agent, and eitherGenerates once or relays aStreamReaderas SSE frames. Long turns (>~30s) should run async via PubSub: the entrypoint//encore:api publicACKs immediately and publishes a turn event; a pubsub worker callsProcessTurnand delivers the reply out-of-band. - Keep LLM keys in
secrets/DB, never in code. Bootstrap the key pool from a secret ininitService.
6. Build loop with engine_* (verify-driven)
- Plan the work with
engine_plan(goal → slices → tasks, each task's verify = a real command). engine_next→ make the edit →engine_verifyruns the verify commands. Use these as the gate:go build ./...— compiles the Encore app + regenerated client.encore test(orgo test ./...) — Encore's test harness provisions the real local DB/infra.encore gen/ a boot ofencore run— regeneratesencore.gen.goto confirm gen matches declared APIs.
engine_advancemarks the task done (commits when configured). Every task MUST pass its verify before advance.
7. Verify the running service (smoke, not a unit test)
encore runboots the app + local Postgres + applies migrations; note the local base URL (defaulthttp://127.0.0.1:4000).- Hit a
publicendpoint:curl -X POST http://127.0.0.1:4000/mypkg.DoThing -d '{...}'. For anauthendpoint, obtain a token from your login endpoint first (or use theX-Access-Key/cookie lane your auth handler accepts). - For a streaming turn:
curl -Nagainst the SSE endpoint and confirmtokenframes arrive incrementally, then a terminaldoneframe. - If there is an admin/dashboard UI, drive it with
browser(open→run: observe the accessibility tree, exercise the changed path, screenshot for visual confirmation). Browser is for the human-facing surface; the API contract is verified bycurl/browser.runfetch.
Acceptance check
-
encore runboots cleanly; migrations apply; no startup panics. -
encore test(orgo test ./...) is green. -
go build ./...succeeds. -
encore.gen.gois freshly regenerated and matches every declared//encore:api/service — no stale gen, never hand-edited (gitignored). - At least one endpoint exercises the Eino graph end-to-end: a request produces a model reply (non-stream) or a stream of
token→doneframes (stream). Tools, if declared, are invoked within the turn. - No secrets, keys, or
__DB_ONLYfallback values are committed.
Non-goals
- Encore.ts (TypeScript). Noted as a sibling with a shared annotation model; not authored, not migrated here.
- Migrating an existing Encore.ts → Go codebase.
- Hand-editing or rewriting generated
encore.gen.go(always regenerate). - Reimplementing omp primitives — this skill composes
codegraph,mcp__codebase_memory_mcp_*,mcp__serena_*,engine_*,browser, and8syncverbs; it adds Encore/Eino procedure on top, never replaces them.