Skip to content

Actions

Actions are the core building blocks for creating and modifying Nostr events in a structured way. An Action is an async function that receives an ActionContext and publishes events directly.

What is an Action?

An action is a function that takes parameters and returns an Action function. The action function receives a context with:

  • events - The event store for reading existing events
  • factory - The event factory for creating and modifying events
  • user - The current user's cast (provides convenient access to user data)
  • self - The current user's public key
  • sign - A helper function to sign events
  • publish - A function to publish events (optionally to specific relays)
  • run - A function to run sub-actions

Actions follow this basic pattern:

ts
import { Action } from "applesauce-actions";

function MyAction(param1: string, param2?: boolean): Action {
  return async ({ events, factory, user, publish, sign }) => {
    // Read existing events from the store
    const existingEvent = events.getReplaceable(kind, user.pubkey);

    // Create or modify events using the factory
    const draft = await factory.modify(existingEvent, ...operations);

    // Sign the event
    const signed = await sign(draft);

    // Publish the event (optionally to specific relays)
    await publish(signed, relays);
  };
}

WARNING

To avoid overriding replaceable events, actions should throw if an existing replaceable event can't be found when expected.

How Actions Publish Events

Actions are responsible for publishing their own events using the publish function from the context. This means:

  1. Actions handle their own publishing - Unlike the old async generator pattern, actions directly call publish() to send events to relays
  2. Relay selection - Actions can specify which relays to publish to by passing a relays array as the second argument to publish()
  3. Outbox support - Most actions publish to the user's outboxes (from user.outboxes$) when available, ensuring events reach the correct relays according to NIP-65

When using ActionRunner.run(), the publish method provided during ActionRunner creation is used. When using ActionRunner.exec(), the publish function in the context emits events to the returned Observable instead.

Pre-built Actions

The applesauce-actions package comes with many pre-built actions for common social client operations. You can find the complete list in the reference.

Some examples include:

  • CreateProfile / UpdateProfile - Managing user profiles
  • FollowUser / UnfollowUser - Managing contact lists
  • BookmarkEvent / UnbookmarkEvent - Managing bookmarks
  • MuteUser / UnmuteUser - Managing mute lists
  • PinNote / UnpinNote - Managing pinned notes

Action Patterns

Creating New Events

When creating a new replaceable event, actions typically check if one already exists:

ts
export function CreateProfile(content: ProfileContent): Action {
  return async ({ events, factory, self, publish, sign }) => {
    const metadata = events.getReplaceable(kinds.Metadata, self);
    if (metadata) throw new Error("Profile already exists");

    const signed = await factory.build({ kind: kinds.Metadata }, setProfileContent(content)).then(sign);
    await publish(signed);
  };
}

Updating Existing Events

When updating events, actions verify the event exists before modifying:

ts
export function UpdateProfile(content: Partial<ProfileContent>): Action {
  return async ({ factory, user, publish, sign }) => {
    // Load the profile and outboxes in parallel
    const [profile, outboxes] = await Promise.all([
      user.profile$.$first(1000, undefined),
      user.outboxes$.$first(1000, undefined),
    ]);

    if (!profile) throw new Error("Profile does not exist");

    const signed = await factory.modify(profile.event, updateProfileContent(content)).then(sign);
    await publish(signed, outboxes);
  };
}

Modifying Tags

Many actions work by adding or removing tags from existing events:

ts
import { firstValueFrom, of, timeout } from "rxjs";

function ModifyContactsEvent(operations: TagOperation[]): Action {
  return async ({ events, factory, user, publish, sign }) => {
    const [event, outboxes] = await Promise.all([
      firstValueFrom(
        events.replaceable(kinds.Contacts, user.pubkey).pipe(timeout({ first: 1000, with: () => of(undefined) })),
      ),
      user.outboxes$.$first(1000, undefined),
    ]);

    const operation = modifyPublicTags(...operations);

    // Modify or build new event
    const signed = event
      ? await factory.modify(event, operation).then(sign)
      : await factory.build({ kind: kinds.Contacts }, operation).then(sign);

    await publish(signed, outboxes);
  };
}

export function FollowUser(user: string | ProfilePointer): Action {
  return ModifyContactsEvent([addProfilePointerTag(user)]);
}

Complex Operations

Some actions perform multiple operations or create multiple events:

ts
export function CreateBookmarkSet(
  title: string,
  description: string,
  additional: { image?: string; hidden?: NostrEvent[]; public?: NostrEvent[] },
): Action {
  return async ({ factory, user, publish, sign }) => {
    const signed = await factory
      .build(
        { kind: kinds.BookmarkList },
        List.setTitle(title),
        List.setDescription(description),
        additional.image ? List.setImage(additional.image) : undefined,
        additional.public ? modifyPublicTags(...additional.public.map(addEventBookmarkTag)) : undefined,
        additional.hidden ? modifyHiddenTags(...additional.hidden.map(addEventBookmarkTag)) : undefined,
      )
      .then(sign);

    await publish(signed, await user.outboxes$.$first(1000, undefined));
  };
}

Creating Custom Actions

To create your own action, define a function that takes parameters and returns an Action function:

ts
import { Action } from "applesauce-actions";
import { kinds } from "applesauce-core/helpers/event";

function SetDisplayName(displayName: string): Action {
  return async ({ factory, user, publish, sign }) => {
    // Get the current profile
    const profile = await user.profile$.$first(1000, undefined);
    if (!profile) throw new Error("Profile not found");

    // Parse existing content
    const content = JSON.parse(profile.event.content || "{}");

    // Update the display name
    content.display_name = displayName;

    // Create a new profile event with updated content
    const signed = await factory
      .modify(profile.event, (event) => {
        event.content = JSON.stringify(content);
        return event;
      })
      .then(sign);

    // Publish the event
    const outboxes = await user.outboxes$.$first(1000, undefined);
    await publish(signed, outboxes);
  };
}

Multi-Event Actions

Actions can publish multiple events if needed:

ts
function CreateUserSetup(profile: ProfileContent, initialFollows: string[]): Action {
  return async ({ factory, user, publish, sign }) => {
    // Create profile
    const profileSigned = await factory.build({ kind: kinds.Metadata }, setProfileContent(profile)).then(sign);
    await publish(profileSigned);

    // Create contacts list
    const contactsSigned = await factory
      .build({
        kind: kinds.Contacts,
        tags: initialFollows.map((pubkey) => ["p", pubkey]),
      })
      .then(sign);

    const outboxes = await user.outboxes$.$first(1000, undefined);
    await publish(contactsSigned, outboxes);
  };
}

Running Sub-Actions

Actions can run other actions using the run function from the context:

ts
function SetupNewUser(profile: ProfileContent, initialFollows: string[]): Action {
  return async ({ run, publish }) => {
    // Run CreateProfile action
    await run(CreateProfile, profile);

    // Run NewContacts action
    await run(NewContacts, initialFollows);
  };
}

Best Practices

  1. Validate inputs - Check that required events exist before attempting modifications
  2. Use factory operations - Leverage the event factory's built-in operations for common tasks
  3. Handle errors gracefully - Throw descriptive errors when preconditions aren't met
  4. Keep actions focused - Each action should have a single, clear responsibility
  5. Document parameters - Use JSDoc comments to describe action parameters and behavior
  6. Publish to outboxes - When available, use the user's outboxes (from user.outboxes$) for publishing to ensure events reach the right relays
  7. Use Promise.all for parallel operations - Load events and outboxes in parallel when possible for better performance

The action pattern allows actions to be composable, testable, and easy to reason about while providing a clean interface for event creation and modification. Actions handle their own publishing, making them self-contained units of work.