Hooks: Feed and Content
The action and filter seams for the activity feed and everything members post into it: posts, comments, reactions, polls, shares, bookmarks, and the composer template. This page is for developers building gamification plugins, moderation integrations, notification bridges, or theme extensions that react to feed activity or modify content before it is written. Every hook below is fired or applied by BuddyNext Free, so it is available without Pro. Several of them are the documented Free-to-Pro extension seams (buddynext_post_pin_limit, buddynext_reaction_types, buddynext_reaction_meta).

Overview / Contract
Section titled “Overview / Contract”- Actions are notifications, not callbacks. BuddyNext fires actions after the database write has committed. Listeners that need the full record should re-fetch by ID (for example
buddynext_service( 'post_service' )->get( $post_id )); the actions pass IDs, not hydrated rows. *_before_savefilters transform or reject. They run after the built-in safeguard checks and receive the candidate data array. Return a modified array to alter what is written, or return aWP_Errorto reject the write outright. Only known columns are persisted; extra keys are ignored.- Actor vs recipient. For engagement that targets someone else’s content, BuddyNext fires an actor-perspective event (who did the thing) and a recipient-mirror alongside it (the content author). Recipient mirrors fire only when the actor is not the author. Gamification systems usually award the recipient.
- Idempotent events fire once. Bookmarks, shares, and votes use
INSERT IGNOREagainst a uniqueness key, and their actions fire only when a row was actually created or changed. Duplicate calls are silent no-ops. - Render and
_part_*template hooks echo at the call site. Filters that return HTML (for examplebuddynext_composer_tools) default to an empty string, so BuddyNext renders nothing unless a plugin hooks. Hooked plugins must return escaped HTML.
Post lifecycle
Section titled “Post lifecycle”| Hook | Type | Fired when | Parameters |
|---|---|---|---|
buddynext_hide_dead_reshares |
filter | A reshare is rendered whose original post is gone (deleted, hidden, blocked, or its author suspended). Default false: the card collapses to a tombstone so the sharer’s action stays legible and the feed does not silently lose posts. Return true to drop it from the feed entirely. |
bool $hide, array $post |
buddynext_post_before_save |
filter | A post is about to be written on create or update, after safeguard checks | array $data, int $user_id, int|null $post_id ($post_id is null on create) |
buddynext_post_created |
action | A new post goes live (also fired for a share, with $type = 'share') |
int $post_id, int $user_id, string $type |
buddynext_post_updated |
action | A post is edited | int $post_id, int $user_id, array $fields (columns written this update) |
buddynext_post_deleted |
action | A post is deleted by its owner | int $post_id, int $user_id |
buddynext_link_preview_max_redirects |
filter | Fetching a link preview, capping how many redirect hops are followed (default 5; every hop is re-validated against SSRF rules before it is followed) |
int $max, string $url |
buddynext_post_approved |
action | A held post is approved by a moderator | int $post_id, int $author |
buddynext_post_rejected |
action | A held post is rejected by a moderator | int $post_id, int $author, string $reason |
buddynext_post_pin_limit |
filter | A user pins a post, to read the per-scope pin cap | int $limit, int|null $space_id, int $user_id (default 1) |
buddynext_user_mentioned |
action | An @username mention in a post (or comment) resolves to a real user |
int $mentioned_user_id, int $mentioner_id, int $post_id |
Note:
buddynext_post_createdis the catch-all “a post exists now” event and fires for every post type, including shares. If you specifically want the share action with the original post ID, listen tobuddynext_post_sharedinstead.
Comments
Section titled “Comments”| Hook | Type | Fired when | Parameters |
|---|---|---|---|
buddynext_comment_before_save |
filter | A comment is about to be written on create or update | array $data, int $user_id, int|null $comment_id ($comment_id is null on create; on update $data carries only content) |
buddynext_comment_created |
action | A new comment is created | int $comment_id, string $object_type, int $object_id, int $user_id |
buddynext_post_comment_received |
action | The author’s post receives a comment from someone else (recipient mirror) | int $comment_id, int $post_id, int $author_id, int $commenter_id |
buddynext_comment_updated |
action | A comment is edited | int $comment_id, int $user_id |
buddynext_comment_deleted |
action | A comment is deleted | int $comment_id, int $user_id |
$object_type is the thing the comment is attached to ('post', 'media', etc.). The post-counter and recipient-mirror logic only run when $object_type === 'post'.
Reactions
Section titled “Reactions”| Hook | Type | Fired when | Parameters |
|---|---|---|---|
buddynext_reaction_added |
action | A reaction is added to an object | string $object_type, int $object_id, int $user_id, string $emoji |
buddynext_post_reaction_received |
action | The author’s post receives a reaction from someone else (recipient mirror) | int $post_id, int $author_id, int $reactor_id, string $emoji |
buddynext_reaction_removed |
action | A reaction is removed from an object | string $object_type, int $object_id, int $user_id, string $emoji |
buddynext_reaction_types |
filter | The allowed reaction-type list is resolved | string[] $types (the owner-enabled subset of the canonical six) |
buddynext_reaction_meta |
filter | A reaction label/char/colour is resolved | array $meta, string $slug ($meta has label, char, color) |
buddynext_reactors_limit |
filter | The reactors list for an object is queried | int $max, string $object_type, int $object_id (default 100) |
The canonical six reaction slugs are like, love, haha, wow, sad, angry. The site owner can disable a subset through Settings, and buddynext_reaction_types runs after that, so the list you receive is already the owner-enabled set. See the example below for adding a custom type.
Polls, shares, and bookmarks
Section titled “Polls, shares, and bookmarks”| Hook | Type | Fired when | Parameters |
|---|---|---|---|
buddynext_poll_vote_before_save |
filter | A poll vote is about to be written | array $data, int $user_id ($data has user_id, post_id, option_id) |
buddynext_poll_voted |
action | A poll vote is cast or switched (not on toggle-off) | int $post_id, int $option_id, int $user_id |
buddynext_share_before_save |
filter | A share is about to be written | array $data, int $user_id ($data has user_id, post_id, content) |
buddynext_post_shared |
action | A post is shared | int $share_id, int $original_post_id, int $user_id |
buddynext_post_bookmarked |
action | A post is bookmarked (first time only) | int $post_id, int $user_id |
buddynext_post_unbookmarked |
action | A bookmark is removed (only when a row was deleted) | int $post_id, int $user_id |
Note:
buddynext_poll_voteddoes not fire when a member clicks the same option again to remove their vote (toggle-off returns early). A vote switch to a different option fires once, after the new vote is recorded.
Feed query and rendering
Section titled “Feed query and rendering”| Hook | Type | Fired when | Parameters |
|---|---|---|---|
buddynext_feed_query_args |
filter | Before a feed query runs (home, profile, space, explore) | array $args, string $scope, int $viewer_id ($args has per_page, cursor, user_id) |
buddynext_feed_order_by |
filter | The home feed ORDER BY clause is built |
string $order_by, int $user_id, array $query_args |
buddynext_feed_items |
filter | A feed page is about to be returned | array $items, string $scope, int $viewer_id, array $args |
buddynext_post_impression |
action | Each item on a served feed page is rendered | int $post_id, int $viewer_id, string $surface (home_feed, profile_feed, space_feed, explore_feed) |
Warning:
buddynext_post_impressionfires once per item per served page and can run dozens of times per request. Keep listeners cheap: defer counting work to Action Scheduler rather than writing to the database inside the hook.
Composer template seams
Section titled “Composer template seams”The composer partial exposes wrapper hooks for adding tools and modals to the post box. These are template-level and run wherever the composer is rendered.
| Hook | Type | Fired when | Parameters |
|---|---|---|---|
buddynext_part_composer_args |
filter | The composer args are assembled | array $args (user_id, space_id, and render flags) |
buddynext_part_composer_before |
action | Immediately before the composer markup | array $args |
buddynext_composer_tools |
filter | The composer toolbar row is rendered | string $html, array $args (return escaped HTML to append a tool button) |
buddynext_composer_modals |
action | After the composer, where tool modals belong | array $args |
buddynext_part_composer_after |
action | Immediately after the composer markup | array $args |
buddynext_comment_descendant_cap |
filter | One page of a comment thread is loaded, bounding how many descendant rows come with it | int $cap, string $object_type, int $object_id |
buddynext_explore_all_deck |
filter | The blended Explore first-page deck is assembled, so an add-on can inject its own cards among the posts | array $items, array $post_cards |
buddynext_post_link_meta_resolved |
action | A queued link preview finished resolving and was stored on the post. The post was already visible without it, so anything that renders the preview should refresh here | int $post_id |
buddynext_scheduled_post_published |
action | A scheduled post is published ahead of its schedule | int $post_id |
Note: Every feed template partial (
post-actions,post-body,post-byline,post-comment-form, and so on) also exposes the standardbuddynext_part_<name>_before/_afteractions andbuddynext_part_<name>_args/_classesfilters. They follow the same convention as the composer hooks and are the safe seams for theme overlays. See Hooks: Overview for the template-part naming contract.
Examples
Section titled “Examples”Add a custom reaction type
Section titled “Add a custom reaction type”buddynext_reaction_types adds the slug; buddynext_reaction_meta supplies its label (and optional emoji char / colour). The slug must be lowercase alphanumeric, and for a complete UI it needs an icon at assets/icons/reaction-{slug}.svg and a --bn-reaction-{slug} colour token. Adding the slug without the icon and CSS leaves the picker broken, so ship both.
// 1. Register the slug.add_filter( 'buddynext_reaction_types', function ( array $types ): array { $types[] = 'celebrate'; return $types;} );
// 2. Give it a human label (and optionally a fallback emoji char + colour).add_filter( 'buddynext_reaction_meta', function ( array $meta, string $slug ): array { if ( 'celebrate' === $slug ) { $meta['label'] = __( 'Celebrate', 'my-addon' ); $meta['char'] = '🎉'; $meta['color'] = '#f5a623'; } return $meta;}, 10, 2 );Raise the pin cap for premium members
Section titled “Raise the pin cap for premium members”Free allows one pinned post per profile (pinning is profile-only). Return a higher integer to lift the cap. $space_id is always null.
add_filter( 'buddynext_post_pin_limit', function ( int $limit, ?int $space_id, int $user_id ): int { // Members in the 'premium' role may pin up to five posts per scope. if ( user_can( $user_id, 'bn_premium_member' ) ) { return 5; } return $limit;}, 10, 3 );Award the post author when their post is reacted to
Section titled “Award the post author when their post is reacted to”Listen to the recipient mirror, not the actor event, so you award the author rather than the reactor.
add_action( 'buddynext_post_reaction_received', function ( int $post_id, int $author_id, int $reactor_id, string $emoji ): void { my_gamification_award_points( $author_id, 5, 'reaction_received' );}, 10, 4 );Reject a post before it is written
Section titled “Reject a post before it is written”Return a WP_Error from buddynext_post_before_save to block the write. The error surfaces in the REST response body.
add_filter( 'buddynext_post_before_save', function ( $data, int $user_id, ?int $post_id ) { if ( null === $post_id && str_contains( (string) ( $data['content'] ?? '' ), 'banned-phrase' ) ) { return new WP_Error( 'blocked', __( 'That phrase is not allowed.', 'my-addon' ), [ 'status' => 422 ] ); } return $data;}, 10, 3 );Notes / gotchas
Section titled “Notes / gotchas”- Free vs Pro. Every hook on this page is fired by Free. The Pro-facing extension seams are
buddynext_post_pin_limit(raise pin caps for plans),buddynext_reaction_types/buddynext_reaction_meta(add reaction types), and thebuddynext_feed_query_args/buddynext_feed_order_by/buddynext_feed_itemstrio (plan-based feed filtering, reranking, sponsored injection). - Re-fetch, do not assume. Action listeners receive IDs. Hydrate via the relevant service (
post_service,comment_service) instead of caching the data you wish had been passed. buddynext_post_createdfires for shares too. Branch on$typeif you only want native posts; usebuddynext_post_sharedfor the share-specific signal with the original post ID.- Counter writes already happen. BuddyNext updates
reaction_count,comment_count, andshare_countitself before firing these actions. Do not re-increment denormalised counters from a listener.

