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.SigningSecretfrom 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 handlerfrom 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,.svcor.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. A429answer’sRetry-Afteris 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.