Skip to content

Push and pull apps

Serve workflows from an HTTPS endpoint that EZGH calls, or run workers that poll EZGH for work.

Updated View as Markdown

An app is one deployment of your code that hosts workflows. Its kind, set when you create it and fixed after, decides how EZGH reaches your code:

Push Pull
How it runs EZGH sends signed HTTPS requests to your endpoint Your workers poll EZGH for tasks
Needs A public HTTPS endpoint on port 443 Outbound HTTPS only
One invocation may take Your platform’s request limit, at most 15 minutes 15 minutes to 24 hours
Credential your code holds The app’s signing secret An API key with workflows.workers.poll
How EZGH learns your workflows A sync after each deploy Each poll announces them
Good for Serverless platforms and web apps (Vercel, Cloudflare Workers, Cloud Run, Lambda) Code behind a firewall, long CPU-bound steps, local development

The workflow code is the same for both; only how you serve it differs.

Push apps

Create the app

Create a push app with its endpoint, https:// on port 443:

import { Client } from "@ezgh/workflows"

const client = new Client({ apiKey, organizationId, projectId })
const { app, signingSecret } = await client.createApp("orders", { endpoint: "https://app.example.com/api/workflows" })
c, err := workflows.NewClient(workflows.ClientOptions{APIKey: apiKey, OrganizationID: orgID, ProjectID: projectID})
created, err := c.CreateApp(ctx, "orders", workflows.CreateAppOptions{Endpoint: "https://app.example.com/workflows"})
// created.App, *created.SigningSecret
from ezgh_workflows import Client

client = Client(api_key=api_key, organization_id=org_id, project_id=project_id)
created = client.create_app("orders", endpoint="https://app.example.com/ezgh-workflows/")
# created["app"], created["signingSecret"]

Or call CreateApp with {"name": "orders", "kind": "push", "endpoint": "…"}. The answer carries the app’s signing secret (whsec_…) once: store it where your code can read it, for example in EZGH_WORKFLOWS_SIGNING_SECRET. A repeated create with the same Idempotency-Key answers signingSecret: null; if you lost the secret, rotate it.

The app starts unsynced. EZGH sends nothing to an app until it has synced.

Serve your workflows

Each SDK serves your workflows from one route, and verifies every request’s EZGH-Signature header against the signing secret, refusing requests more than 5 minutes old:

import { serve } from "@ezgh/workflows"

// A Fetch API handler, (Request) => Promise<Response>: Bun, Node, Deno, Cloudflare Workers, Next.js
export const POST = serve({
  workflows: [onboarding],
  signingSecrets: [process.env.EZGH_WORKFLOWS_SIGNING_SECRET],
  maxInvocationSeconds: 300,
})
h, err := workflows.Handler(workflows.HandlerOptions{
	Workflows:            []workflows.Workflow{onboarding},
	SigningSecrets:       []string{os.Getenv("EZGH_WORKFLOWS_SIGNING_SECRET")},
	MaxInvocationSeconds: 300,
})
http.Handle("/workflows", h) // a net/http handler
from ezgh_workflows import serve

# An ASGI app: run it with uvicorn, or mount it in Starlette or FastAPI
app = serve(
    workflows=[onboarding],
    signing_secrets=[os.environ.get("EZGH_WORKFLOWS_SIGNING_SECRET")],
    max_invocation_seconds=300,
)

maxInvocationSeconds is how long your platform lets a request run, 1 to 900 seconds (default 300). EZGH sets each invocation’s deadline inside it, and the SDK stops starting steps when less than 10 % of the time (at least 5 seconds) is left; EZGH then invokes the workflow again. A step can’t outlast its invocation, so steps longer than your request limit need a pull worker.

Sync after each deploy

A sync tells EZGH your endpoint and its workflows. EZGH sends a signed sync request to the endpoint, and the SDK answers with the workflows it serves, their retry policies and timeouts. Run it from your deploy pipeline:

await client.sync(appId, { endpoint: "https://app.example.com/api/workflows" })
app, err := c.Sync(ctx, appID, workflows.SyncOptions{Endpoint: "https://app.example.com/workflows"})
client.sync(app_id)

SyncApp takes an optional endpoint (else the app’s) and deployment. The endpoint changes only when a sync succeeds: a sync that fails is 409 sync_failed and changes nothing, so an app can’t be pointed at an address that didn’t answer as an EZGH SDK with the app’s secret. Runs start only for workflows the last sync reported: starting a run of an app that never synced is 409 app_not_synced, and of a workflow it didn’t report 409 workflow_not_synced.

Rotate the signing secret

RotateAppSecret returns a new secret, once. The previous secrets stay valid for 24 hours, and requests are signed with every valid secret, so deploy the new one alongside the old (each SDK takes a list of secrets), then remove the old one.

Endpoints EZGH calls

  • HTTPS on port 443, with a publicly trusted certificate. Redirects aren’t followed.
  • Not IP addresses, localhost, or names under .local, .internal, .test, .svc or .localhost, and never an address that resolves to a private, loopback, link-local or shared address.
  • An answer is at most 16 MiB.

When your endpoint fails

  • A step had started in the failed invocation: that’s a failed attempt of the step, retried under its retry policy.
  • No step had started (the endpoint was unreachable, or your code crashed before its first step): the invocation is retried with backoff, at most 10 minutes apart, for 24 hours. Then the run fails with invocation_failed. A 429 answer’s Retry-After is honored.
  • Every invocation of the app fails for 5 minutes: EZGH pauses delivery to the app, marks it unhealthy, and tries a sync request once a minute. Delivery resumes once one succeeds; runs wait meanwhile instead of using up their retries.

Pull apps

Create a pull app with "kind": "pull", an optional task queue (default default) and partitions (default 4). It has no endpoint, signing secret or sync. Workers poll its queue with an API key and announce the workflows they serve. See Pull workers and the quickstarts.

Delete an app

DeleteApp deletes the app and its schedules.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close