this.emit() publishes an event to one of your workspace's event sources. Every deployment
subscribed to that source runs with the payload you published.
async chargeCard() {
const result = await charge(this.payload);
await this.emit("order.paid", { orderId: result.id, amount: result.amount });
return result;
}
The emitting flow does not wait for the subscribers. Fan-out happens on the platform's dispatch
queue after the event is recorded, so emit resolving means the event exists, not the
subscribers finished.
This is how you decompose work: one flow does the thing, several unrelated flows react to it, and none of them knows about the others.
const res = await this.emit("order.paid", payload);
// { eventId, subscribers, deduped, inactive }
eventId — the recorded event, or null if nothing was recorded.subscribers — how many active subscriptions existed when the event was recorded. It is a
snapshot, not a result: they had not run yet.deduped — an earlier event with the same idempotency key was returned instead.inactive — the source exists but is switched off.Switching a source off must not break every flow that mentions it, so an emit to an inactive
source returns { eventId: null, inactive: true } and your flow carries on. Branch on it if it
matters:
const res = await this.emit("order.paid", payload);
if (res.inactive) this.log("WARN", "order.paid is switched off; nothing was recorded");
Real failures are thrown: an unknown key, a payload the source's schema refuses, and an emit too deep inside an event cascade.
A step can be retried, and a naive emit inside one publishes twice. Pass an idempotency key:
await this.emit("order.paid", payload, { idempotencyKey: `order-${order.id}` });
A second emit with the same key inside the platform's dedupe window returns the first event and
dispatches nothing. deduped: true tells you that happened.
An event's payload can be raw bytes instead of JSON:
import { Workflow, binary } from "konnecthub/flow";
const res = await fetch(url);
await this.emit("file.received", binary(res.body!, "application/pdf"), {
filename: "invoice.pdf",
meta: { url },
});
Bytes are streamed to the platform chunk by chunk, and each chunk is acknowledged only once stored — so a slow store throttles the producer instead of filling the worker's memory. A stream interrupted by a throwing step or a dead worker records no event at all, rather than a truncated one.
meta is a JSON sidecar stored alongside a binary payload. It is how bytes carry structured
context, since the bytes themselves describe nothing.
On the other side, a deployment subscribed to the source runs with the published payload as
this.payload, and this.event describes the delivery:
handle() {
const ctx = this.event; // undefined for a webhook, schedule or manual run
if (!ctx) return;
this.log("INFO", `from ${ctx.source} at depth ${ctx.depth}`, { eventId: ctx.eventId });
}
depth is the cascade depth: 0 for a webhook or manual origin, and one more for each emitting
subscriber in the chain. It is the guard against an emit loop — a flow that emits to the source
that triggered it will eventually be refused rather than running forever.
subscribers: 0, so testing can never start production runs.