External access

External access

External access lets people outside your team run a flow, either through a signed link you hand out or, for a public flow, without any link at all. This is useful for flows that collect input from customers, such as a feedback form or an onboarding step.

External access is in preview, and its API is subject to change.

Making a flow runnable externally

Mark a flow with @externalAccess to let people outside your team run it through a signed link:

flow ShareableFlow {
    inputs {
        name Text?
    }
    @externalAccess
    @permission(actions: [share], roles: [Admin])
}

A flow with @externalAccess is not openly runnable. An authorised user mints a signed link, and whoever holds the link can run the flow without signing in.

To make a flow runnable by anyone without a link, use @externalAccess(public: true):

flow PublicRunFlow {
    @externalAccess(public: true)
}

A public flow's run action requires no permission, so any caller, including an unauthenticated one, can start it.

The share permission

Minting, listing, and revoking signed links through the Console, or from code as a specific user, is gated by a share permission action. Only the roles you allow can hand out access:

flow ShareableFlow {
    inputs {
        name Text?
    }
    @externalAccess
    @permission(actions: [share], roles: [Admin])
}

Minting from code with the SDK default is the exception. Those calls run as a trusted service and do not require the share permission, as described in Minting links in code.

Public and shareable are not mutually exclusive. A public flow can also mint shareable links, which is useful for baking per-record default inputs into a link, for example a per-order feedback link. Minting through the Console or as a specific user is still gated by the share permission even when the flow is public; being public only opens the run action.

flow PublicShareableFlow {
    inputs {
        name Text?
    }
    @externalAccess(public: true)
    @permission(actions: [share], roles: [Admin])
}

Single-use and reusable links

A single-use link counts as used only once a run completes. A visitor who abandons or fails a run can retry the same link until one run succeeds. Reusable links can be run any number of times.

Minting links in code

Along with minting a link by hand in the Console, the SDK can mint, list, and revoke links from your own code. This is useful when a link should be created as a side effect of something else, such as generating a per-order feedback link when an order is dispatched.

Every flow declared with @externalAccess exposes a signedLinks namespace on its SDK client, reached through the flows export. The namespace is absent on flows without @externalAccess, so referencing it there is a compile error.

signedLinks.create mints a link and returns a record. Its url field is the shareable link to hand out. All of its options are optional:

  • inputs: default inputs applied to every run started from the link. A run can still override them.
  • reusable: whether the link can be opened more than once. Links are single-use by default.
  • expiresAt: a Date after which the link stops working.

The example below mints a reusable link for a CustomerFeedback flow from inside another flow and stores its URL on an order. The mint runs inside a step so it is not repeated when the flow replays:

import { DispatchOrder, flows, models } from "@teamkeel/sdk";
 
export default DispatchOrder({}, async (ctx, inputs) => {
  const link = await ctx.step("mint feedback link", async () => {
    const created = await flows.customerFeedback.signedLinks.create({
      reusable: true,
      inputs: { orderId: inputs.orderId },
    });
 
    await models.order.update(
      { id: inputs.orderId },
      { feedbackUrl: created.url! }
    );
 
    return { url: created.url! };
  });
 
  return ctx.complete({ title: "Order dispatched", data: link });
});

The same flows.<flow>.signedLinks API is available from a custom function or a subscriber. By default these calls run as a trusted flows service and bypass the flow's share permission, even when the flow is public, so backend code can mint links even when the caller holds no share permission. Links minted this way have no owner, so their createdBy is null.

To run a call as a specific user and enforce the share permission, set one of the following:

  1. an auth token with .withAuthToken(token)
  2. an identity with .withIdentity(identity)

With either set, the call is scoped to that user and the flow's share permission applies. For example, to mint as the user running the flow, pass their identity:

const link = await flows.customerFeedback
  .withIdentity(identity)
  .signedLinks.create({ reusable: true });

Use list and revoke to manage existing links. list takes an optional status filter of active, revoked, or expired:

const { results } = await flows.customerFeedback.signedLinks.list({
  status: "active",
});
 
await flows.customerFeedback.signedLinks.revoke(results[0].id);

External-facing copy

A flow's internal title and description are shown to authenticated Console users. To avoid exposing internal wording to anonymous visitors, set external copy in an externalAccess block in the flow's config:

import { ShareableFlow } from "@teamkeel/sdk";
 
export default ShareableFlow(
  {
    title: "Internal review flow",
    externalAccess: {
      title: "Leave your feedback",
      description: "Tell us about your order",
      introduction: "This takes about a minute.",
      branded: true,
    },
  },
  async (ctx) => {
    return ctx.complete({ title: "Thanks for your feedback" });
  },
);

For more on writing flows, see Writing flows and Permissions.