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 eventsfactory- The event factory for creating and modifying eventsuser- The current user's cast (provides convenient access to user data)self- The current user's public keysign- A helper function to sign eventspublish- A function to publish events (optionally to specific relays)run- A function to run sub-actions
Actions follow this basic pattern:
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:
- Actions handle their own publishing - Unlike the old async generator pattern, actions directly call
publish()to send events to relays - Relay selection - Actions can specify which relays to publish to by passing a
relaysarray as the second argument topublish() - 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 profilesFollowUser/UnfollowUser- Managing contact listsBookmarkEvent/UnbookmarkEvent- Managing bookmarksMuteUser/UnmuteUser- Managing mute listsPinNote/UnpinNote- Managing pinned notes
Action Patterns
Creating New Events
When creating a new replaceable event, actions typically check if one already exists:
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:
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:
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:
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:
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:
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:
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
- Validate inputs - Check that required events exist before attempting modifications
- Use factory operations - Leverage the event factory's built-in operations for common tasks
- Handle errors gracefully - Throw descriptive errors when preconditions aren't met
- Keep actions focused - Each action should have a single, clear responsibility
- Document parameters - Use JSDoc comments to describe action parameters and behavior
- Publish to outboxes - When available, use the user's outboxes (from
user.outboxes$) for publishing to ensure events reach the right relays - 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.
