Ory-authed Temporal TypeScript worker
You are helping the user scaffold a Temporal TypeScript worker where every Activity execution is gated by Ory: the user is authenticated, the worker's agent identity is resolved via OAuth2 Dynamic Client Registration, each Activity invocation runs an Ory Permission check, and the full lifecycle emits structured activity events. Workflows stay deterministic — only Activities call out to Ory.
This skill carries the workflow. You generate the files in the user's repo; the user runs the Temporal CLI and the worker.
Precondition: Node.js 20+ is installed, and the user has installed (or will install) the Temporal CLI. On macOS:
brew install temporal. On Linux/Windows: download from https://temporal.download and puttemporalonPATH. If the binary is missing, point the user at the docs and stop — do not fabricate it.
Step 1 — Confirm the target
Before writing files, confirm with the user:
- Where the worker should live. Default to
temporal-worker/at the repo root unless the user says otherwise. - Task queue name. Default
agent-tools. The worker and the workflow starter must agree on this string. - Ory project URL. For local development point at the local Ory stack
(
/ory-agent-plugin:local-up) onhttp://localhost:4000. For Ory Network, the user plumbs their project URL through the worker's env. Either way the worker needs network reachability to the Ory APIs at runtime.
Step 2 — Scaffold the Temporal project
Run the official Temporal scaffold and select the hello-world sample when
prompted. Then add the Ory agent client:
npx @temporalio/create@latest temporal-worker
cd temporal-worker
npm install @ory/argus
The scaffold lays down src/{activities.ts, workflows.ts, worker.ts, client.ts}
and pre-configures the Worker to connect to the Temporal dev server at
localhost:7233. The Web UI lives at http://localhost:8233 once the dev
server is running.
Step 3 — Wrap Activities with the Ory gate
Activities are where side effects happen, so they are the right place to put the permission check. Never call Ory from inside a Workflow — Workflows are deterministic and replayed; a live Ory call would break replay safety.
Replace the scaffold's src/activities.ts with:
import { Context } from "@temporalio/activity";
import {
OryAgentClient,
ensureUserAuthenticated,
ensureAgentIdentity,
checkAndDecide,
resolveUserSubject,
subjectLabel,
} from "@ory/argus";
// One client per worker process. `harness` is a label that shows up on
// every activity event so worker-originated audit lives in its own namespace
// alongside the CLI plugins.
const ory = OryAgentClient.fromEnv("temporal");
// Run the user + agent gates exactly once per process. Activities call
// `bootstrap()` lazily and await the same promise on subsequent calls.
let bootstrapped: Promise<void> | undefined;
function bootstrap(): Promise<void> {
if (!bootstrapped) {
bootstrapped = (async () => {
await ensureUserAuthenticated(ory, {
binName: "temporal-worker",
harness: "temporal",
// The user gate runs every invocation and never blocks: it refreshes
// tokens and emits the audit event, but the worker proceeds even if the
// user is unauthenticated. Enforcement happens at the per-Activity
// permission check (permissionMode).
});
await ensureAgentIdentity(ory, {
projectUrl: process.env.ORY_PROJECT_URL,
});
})();
}
return bootstrapped;
}
async function gate(toolName: string, userSubject: string): Promise<void> {
await bootstrap();
const subject = resolveUserSubject(ory, userSubject);
const decision = await checkAndDecide(
ory,
{
namespace: process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTool",
object: toolName,
relation: "use",
...subject,
},
{
activityAttributes: {
toolName,
workflowId: Context.current().info.workflowExecution.workflowId,
activityId: Context.current().info.activityId,
},
}
);
switch (decision.kind) {
case "allow":
case "observe":
case "fail_open":
return;
case "deny":
throw new Error(
`Ory denied use of ${toolName} for ${subjectLabel(subject)}`
);
}
}
export async function sendEmail(input: {
user: string;
to: string;
subject: string;
}): Promise<string> {
await gate("send_email", input.user);
// …real side effect here…
return `sent to ${input.to}`;
}
Key choices:
- Activities call Ory, Workflows don't. Anything that needs a live decision goes in an Activity. Workflows only orchestrate.
- The user gate never blocks. It runs every invocation to refresh the user identity, but always proceeds. Enforcement is at the permission check, which throws on deny — Temporal will mark the Activity as failed and surface the error via the Workflow result or retry policy.
harness: "temporal". Distinguishes worker-originated events in the unified activity log.- Activity attributes carry the Workflow + Activity IDs. This is how operators correlate Ory denials back to Temporal executions in the Web UI.
Step 4 — Pass the user subject through the Workflow
Workflows must be deterministic, so they cannot read process.env or call out
to Ory. The user subject travels as Workflow input. Update src/workflows.ts:
import { proxyActivities } from "@temporalio/workflow";
import type * as activities from "./activities";
const { sendEmail } = proxyActivities<typeof activities>({
startToCloseTimeout: "1 minute",
});
export async function notifyUser(input: {
user: string;
to: string;
}): Promise<string> {
return sendEmail({
user: input.user,
to: input.to,
subject: "hello",
});
}
And src/client.ts:
import { Connection, Client } from "@temporalio/client";
import { nanoid } from "nanoid";
import { notifyUser } from "./workflows";
const client = new Client({ connection: await Connection.connect() });
const handle = await client.workflow.start(notifyUser, {
taskQueue: "agent-tools",
workflowId: `notify-${nanoid()}`,
args: [{ user: "user@example.com", to: "alice@example.com" }],
});
console.log("workflow started:", handle.workflowId);
console.log("result:", await handle.result());
src/worker.ts stays as the scaffold writes it — Worker.create already
registers Workflows and Activities together and listens on the task queue.
Step 5 — Run it
Three terminals. See /ory-agent-plugin:local-up for the Ory side; the Temporal side uses
the Temporal CLI's bundled dev server (Postgres + Temporal Server + Web UI in
one process):
# Terminal 1 — local Ory stack (Kratos, Keto, Hydra, gateway)
`/ory-agent-plugin:local-up`
# Terminal 2 — local Temporal dev server (Web UI at http://localhost:8233)
temporal server start-dev
# Terminal 3 — the worker, pointed at both
cd temporal-worker
export ORY_PROJECT_URL=http://localhost:4000
export ORY_AUTH_GATE=1
export ORY_AGENT_DEBUG=true
export ORY_AGENT_LOG_FILE=$PWD/ory-agent-debug.log
npm run start # boots the worker, polls task queue
In a fourth terminal, kick the workflow once:
cd temporal-worker
npm run workflow
Tail the activity log to confirm the gates fired:
tail -f ory-agent-debug.log | jq .
You should see:
- exactly one
user.authactivity event (the worker's first Activity triggeredbootstrap()), - exactly one
agent.authactivity event, - one
tool.invoke(allow) ortool.block(deny) event per Activity execution.
The Workflow itself produces no Ory activity — only its Activities do.
Step 6 — Promotion from observe to enforce
The worker starts in observe mode by default: explicit blocks pass through but
each is recorded as a permission.block_observed activity event. Once the user
has confirmed the block set is what they expect, an admin promotes the posture to enforce in
the Ory Console (Agent Security). It is a permission on the project, read on
every session, so the worker picks it up on its next run with no redeploy.
Activities are allowed by default. Add blockedSubjects relations for Activity
names that must not run, then review them before promoting the posture. See
/project:ory-permissions-onboarding.
To exercise the deny path locally, add a blockedSubjects relation for an
Activity name, kick the Workflow, and watch the explicit-block error in the
Temporal Web UI.
Step 7 — Beyond the dev server
This skill stops at the local dev server. For production:
- Pin a static agent identity with
ORY_AGENT_CLIENT_ID + ORY_AGENT_CLIENT_SECRET(client_credentials) so the worker doesn't re-register on every cold start. - Set retention and access controls for the worker's unified activity log. Ory Agent Security owns service telemetry; the worker does not export traces.
- Use Temporal Cloud or a self-hosted Temporal cluster instead of
temporal server start-dev; the worker code does not change.
What this skill does NOT do
- It does not modify the user's Ory project — use
/project:ory-auth-setupfor that. - It does not call Ory from inside a Workflow. Workflows are deterministic and must never make non-deterministic calls; all gating lives in Activities.
- It does not deploy the worker. The user runs
temporal server start-devandnpm run startlocally; production deployment is out of scope. - It does not pin Temporal or
@ory/argusversions. For reproducibility, pin both in the generatedpackage.jsonbefore committing.