Waiting for External Input in an Effect Golem Agent

SkillAI & models

Lets your agent pause a workflow and wait durably until a human or another agent supplies data.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Waiting for External Input in an Effect Golem Agent skill

About this capability

Waiting for external input with durable promises in an Effect-based Golem agent. Use for human-in-the-loop workflows, callbacks, external decisions, or suspending an @golemcloud/effect-golem invocation until another agent supplies data.

What this skill tells your AI

The instructions your AI receives, as published by golemcloud/golem in golem-skills/skills/effect/golem-wait-for-external-input-effect/SKILL.md and read by ahel’s review.

Use Agents.Promises when an invocation must durably suspend until another agent or external system supplies a one-shot byte payload. Golem owns the promise and suspension, so the waiting agent consumes no compute while idle and can recover through normal durable execution.

These are Golem host promises, not JavaScript Promise values or Effect Deferred values. An ordinary Deferred, fiber, queue, or callback is in-memory coordination and must not be used as a durable external waiting handle.

API

Import the public namespace from the Effect SDK:

import { Effect } from "effect";
import { Agents } from "@golemcloud/effect-golem";
APIBehavior
Agents.Promises.createEffect that allocates a durable promise and returns Agents.PromiseId
Agents.Promises.await(id)Effect that durably waits and returns the completed Uint8Array
Agents.Promises.poll(id)Non-blocking Effect returning Uint8Array | undefined
Agents.Promises.complete(id, bytes)Effect that completes the promise once and returns true

create is itself an Effect value, so yield it without parentheses:

const promiseId = yield * Agents.Promises.create;
const bytes = yield * Agents.Promises.await(promiseId);

Only the agent that created a promise may await or poll it. Another agent may complete the shared PromiseId. Completion is one-shot; another completion fails with Agents.PromiseAlreadyCompletedError.

Encode and Decode the Payload

Golem promises carry bytes. Define the application protocol explicitly at the boundary:

const payload = new TextEncoder().encode("approved");
yield * Agents.Promises.complete(promiseId, payload);

const bytes = yield * Agents.Promises.await(promiseId);
const decision = new TextDecoder().decode(bytes);

For structured data, encode JSON before completion and parse and validate it after awaiting. Do not hide malformed external input by substituting an empty or default value.

Pass a Promise ID Through Agent RPC

Agents.PromiseId is a public TypeScript type but this SDK revision does not export a ready-made Effect Schema for it. Agent method parameters need a Schema, so reproduce the host type's exact structural shape:

import { Schema } from "effect";
import { WitTypes } from "@golemcloud/effect-golem";

const PromiseIdSchema = Schema.Struct({
  agentId: Schema.Struct({
    componentId: Schema.Struct({
      uuid: Schema.Struct({
        highBits: WitTypes.Uint64,
        lowBits: WitTypes.Uint64,
      }),
    }),
    agentId: Schema.String,
  }),
  oplogIdx: WitTypes.Uint64,
});

Use WitTypes.Uint64, not Schema.BigInt: the UUID halves and oplog index are WIT u64 values. Pass only this plain data over RPC. Do not pass an Effect, fiber, host pollable, or JavaScript promise.

Full Example: Human-in-the-Loop Approval

This pattern uses two durable agents. ApprovalAgent.request creates the promise, hands its ID to the matching DeciderAgent, and then awaits it. A later HTTP call to DeciderAgent.decide completes the promise and resumes the approval invocation.

// src/approval-agents.ts
import { Effect, Ref, Schema } from "effect";
import {
  Agents,
  defineAgent,
  Http,
  method,
  WitTypes,
} from "@golemcloud/effect-golem";

const PromiseIdSchema = Schema.Struct({
  agentId: Schema.Struct({
    componentId: Schema.Struct({
      uuid: Schema.Struct({
        highBits: WitTypes.Uint64,
        lowBits: WitTypes.Uint64,
      }),
    }),
    agentId: Schema.String,
  }),
  oplogIdx: WitTypes.Uint64,
});

export const DeciderAgent = defineAgent({
  name: "DeciderAgent",
  mode: "durable",
  id: {
    name: Schema.String,
  },
  http: Http.mount("/deciders/{name}"),
  methods: {
    receive: method({
      input: { promiseId: PromiseIdSchema },
      success: Schema.Void,
      http: [Http.post("/receive")],
    }),
    decide: method({
      input: { decision: Schema.String },
      success: Schema.Boolean,
      http: [Http.post("/decide")],
    }),
  },
}).implement({
  init: () => Ref.make<Agents.PromiseId | undefined>(undefined),
  methods: (pending) => ({
      receive: ({ promiseId }) => Ref.set(pending, promiseId),

      decide: ({ decision }) =>
        Effect.gen(function* () {
          const promiseId = yield* Ref.get(pending);
          if (promiseId === undefined) return false;

          const completed = yield* Agents.Promises.complete(
            promiseId,
            new TextEncoder().encode(decision),
          ).pipe(
            Effect.catchTag("PromiseAlreadyCompletedError", () =>
              Effect.succeed(false),
            ),
          );

          if (completed) yield* Ref.set(pending, undefined);
          return completed;
        }).pipe(Effect.orDie),
  }),
});

export const ApprovalAgent = defineAgent({
  name: "ApprovalAgent",
  mode: "durable",
  id: {
    name: Schema.String,
  },
  http: Http.mount("/approvals/{name}"),
  methods: {
    request: method({
      input: {},
      success: Schema.String,
      http: [Http.post("/request")],
    }),
    getResult: method({
      input: {},
      success: Schema.String,
      http: [Http.get("/result")],
    }),
  },
}).implement({
  init: ({ name }) =>
    Effect.gen(function* () {
      const result = yield* Ref.make("");
      return { name, result };
    }),
  methods: ({ name, result }) => ({
      request: () =>
        Effect.gen(function* () {
          const promiseId = yield* Agents.Promises.create;
          const decider = yield* DeciderAgent.client.get({ name });

          // Await this short handoff so the decider has stored the ID before we suspend.
          yield* decider.receive({ promiseId });

          const bytes = yield* Agents.Promises.await(promiseId);
          const decision = new TextDecoder().decode(bytes);
          yield* Ref.set(result, decision);
          return decision;
        }).pipe(Effect.orDie),

      getResult: () => Ref.get(result),
  }),
});

Register the module for side effects:

// src/main.ts
import "./approval-agents.js";

Expose both agents in the HTTP API without removing existing deployment entries:

httpApi:
  deployments:
    local:
      - domain: test-app.localhost:9006
        agents:
          ApprovalAgent: {}
          DeciderAgent: {}

With these method schemas, POST /deciders/a1/decide accepts the normal JSON body { "decision": "approved" }. The waiting /approvals/a1/request invocation resumes, stores "approved", and returns it.

Handoff Choices

An awaited handoff confirms that the receiver stored the ID before the creator starts waiting:

yield * decider.receive({ promiseId });

If acknowledgement is unnecessary, fire-and-forget handoff is also available:

yield * decider.receive.trigger({ promiseId });

Both client.get(...) and the remote call are lazy Effects and must be yielded. Avoid synchronous RPC cycles: the completing agent must not call back and await a method on the already-suspended creator.

Lifecycle and Error Rules

  • Create a fresh Golem promise for each one-shot wait; do not reuse a completed ID.
  • Ensure every created promise has a completion path. Interrupting Agents.Promises.await stops only the JavaScript wait; the host promise remains pending.
  • Do not add polling loops or manual retries around await; let Golem persist the suspension and replay durable execution.
  • Keep PromiseId, URLs, decisions, and other plain values in agent state. Never store Effects, Deferred, fibers, or host resource handles in snapshot state.
  • create, await, and complete expose typed SDK host errors. Map them to a declared domain error when callers should recover, or deliberately use Effect.orDie when infrastructure failure should fail and retry the invocation.
  • Use poll only when a non-blocking readiness check is required. Prefer await for durable suspension instead of repeatedly polling.
  • Import every implemented agent module from src/main.ts, and do not edit generated files under golem-temp/.

Signals

GitHub stars
2k
Forks
211
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
golem-wait-for-external-input-effect
Source
github.com/golemcloud/golem