Skip to content

feat(actions): Add verify_action returning a typed action context - #776

Open
gjtorikian wants to merge 2 commits into
mainfrom
feat/actions-typed-context
Open

gjtorikian wants to merge 2 commits into
mainfrom
feat/actions-typed-context

Conversation

@gjtorikian

Copy link
Copy Markdown
Contributor

Description

Adds client.actions.verify_action(), which verifies an AuthKit Actions request and deserializes it into a typed AuthenticationActionContext or UserRegistrationActionContext. This matches what the webhooks module already does with verify_event(). Until now Actions only offered construct_action(), which returns an untyped dict, so customers had to discover field names from example requests and index into the dict by hand.

from workos.actions import AuthenticationActionContext

action = client.actions.verify_action(
    payload=request.body,
    sig_header=request.headers["WorkOS-Signature"],
    secret=ACTIONS_SECRET,
)
if isinstance(action, AuthenticationActionContext):
    action.user.email
    action.organization_membership.role.slug
else:
    action.user_data.email
    action.invitation.state

Non-breaking. construct_action(), verify_header(), and sign_response() keep their signatures and behavior. AsyncActions is now a subclass of Actions instead of a duplicated copy.

Why dedicated models instead of the generated REST models

The Actions payload is not part of the OpenAPI spec, so it cannot be generated. The shapes also differ from the REST models in ways that matter:

  • A real Actions request carries a trimmed organization_membership (no roles) and a trimmed invitation (no token or accept_invitation_url).
  • The dashboard's "send test request" sends only the fields the Actions contract guarantees, so user and organization arrive without external_id, metadata, and similar REST-only fields.

The generated User, Organization, OrganizationMembership, and Invitation models have a strict from_dict that would raise on every one of those requests. The new Action* dataclasses require only what the contract guarantees and treat the production extras as optional. They reuse the generated enums and SlimRole, which already tolerate unknown values. The module header in src/workos/actions.py records this so nobody "fixes" it back to the REST models.

Tests

tests/test_actions.py now runs against realistic fixtures: a production authentication payload, a dashboard test-request payload, a user-registration payload with an invitation, and a minimal one without. It covers the sync and async client paths and rejects bad signatures, stale timestamps, non-object JSON, and unknown object values.

Check Result
ruff check / ruff format --check clean
pyright (strict, src + tests) 0 errors
Full suite, Python 3.11 3809 passed
tests/test_actions.py, Python 3.10 31 passed

Documentation

Does this require changes to the WorkOS Docs? E.g. the API Reference or code snippets need updates.

[ ] Yes

Not required: the change is additive and construct_action() keeps working as documented. A follow-up docs PR can switch the Python snippet on the AuthKit Actions page to verify_action() once this ships. The README gains an "AuthKit Actions" section in this PR.

AuthKit Actions requests were only reachable through construct_action,
which returns the verified payload as an untyped dict. Customers had to
discover field names from example requests and index into the dict by
hand, with no type checking or editor support.

verify_action verifies the signature the same way and deserializes the
payload into AuthenticationActionContext or UserRegistrationActionContext,
discriminated on the object field. construct_action, verify_header, and
sign_response keep their current signatures and behavior.

The Action models are dedicated dataclasses rather than the generated
REST models. The API sends a trimmed organization_membership (no roles)
and a trimmed invitation (no token or accept_invitation_url), and the
dashboard test request strips user and organization further. The
generated models' strict from_dict would raise on every real request,
so these models require only what the actions-types contract guarantees
and treat the production extras as optional.

AsyncActions is now a subclass of Actions instead of a duplicated copy.
@gjtorikian
gjtorikian requested review from a team as code owners November 10, 2026 17:58
@gjtorikian
gjtorikian requested a review from mattgd November 10, 2026 17:58
@greptile-apps

greptile-apps Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no actionable issues were found.

Summary

Adds client.actions.verify_action() with typed contexts for sign-in and sign-up requests.

  • Signed Actions requests now return typed sign-in or sign-up contexts.

No actionable issues were found. Review was based on source inspection; tests were not run.

Reviews (1) · Last reviewed commit: "chore(actions): Describe the payload con..." · Reviewed by Greptile

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants