Post Actions
This guide will explain how to use Post Actions and how to implement custom ones.
Post Actions are contracts that extend the Lens Protocol functionality by allowing Accounts to execute actions on Posts.
Lens provides two built-in Post Actions:
SimpleCollectAction - Allows an Account to collect an NFT from a Post.
TippingPostAction - Allows an Account to tip the author of a Post.
It is also possible to create custom Post Actions to extend the functionality of the Lens Protocol.
Configuring Post Actions
You can configure one or more Post Actions when creating a Post.
This section presumes you are familiar with the process of creating a Post on Lens.
Simple Collect Action
The SimpleCollectAction allows an user to collect an ERC-721 NFT from a given Post.
The NFT collection is eagerly deployed when the Post is created.
The Post author can configure the following parameters:
Collect limit (optional) – The maximum number of NFTs that can be minted.
End time (optional) – The deadline after which NFTs can no longer be collected.
Follower requirement (optional) – Whether the collector must be following the Post author on a specified Lens Graph (global or custom).
NFT immutability (optional) – If enabled, minted NFTs will retain a snapshot of the content URI at the time of minting. This ensures that any edits to the Post after minting will not affect previously minted NFTs. For this to be fully effective, the content URI itself should be immutable.
- Price (optional) – The cost of the NFT. If set, the author can also specify:
Referral share (optional) – The percentage of the payment allocated to any referrers.
Recipients – A list of addresses that will receive a share of the payment. This is after the referral share has been deducted.
For paid collects, a 1.5% Lens treasury fee is deducted from the total amount paid by the collector before calculating referral and recipient shares.
- TypeScript
- GraphQL
Tipping Post Action
The TippingPostAction is enabled by default on any Post, so you don't need to configure it explicitly.
A 1.5% Lens treasury fee is deducted from the tip amount before transferring it to the author.
Custom Post Actions
- TypeScript
- GraphQL
Custom Post Action
import { blockchainData, evmAddress, uri } from "@lens-protocol/client";
const result = await post(sessionClient, { contentUri: uri("lens://4f91ca…"), actions: [ { unknown: { address: evmAddress("0x1234…"), params: [ { raw: { // 32 bytes key (e.g., keccak(name)) key: blockchainData("0xac5f04…"), // an ABI encoded value value: blockchainData("0x00"), }, }, ], }, }, ],});
Executing Post Actions
To execute a Post Action, follow these steps.
You MUST be authenticated as Account Owner or Account Manager to execute a Post Action.
First, inspect the post.actions field to determine what Post Actions are available on a given Post.
Post Actions
for (const action of post.actions) { switch (action.__typename) { case "SimpleCollectAction": // The Post has a Simple Collect Action break;
case "UnknownAction": // The Post has a Custom Post Action break; }}
An example of each Post Action type is provided below.
Next, execute the desired Post Action.
- TypeScript
- GraphQL
- React
Use the executePostAction action to execute any Post Action.
- TypeScript
- GraphQL
- React
Then, handle the result using the adapter for the library of your choice:
See the Transaction Lifecycle guide for more information on how to determine the status of the transaction.
That's it—you've successfully executed a Post Action.
Action Execution History
The Lens Protocol provides a method to track which accounts have executed specific actions on a Post.
- TypeScript
- GraphQL
- React
Use the paginated fetchWhoExecutedActionOnPost action to get a list of accounts that executed a specific Action on a Post.
Enable/Disable Post Actions
To enable/disable a Post Action, follow these steps.
You MUST be authenticated as the Account Owner or Account Manager of the Post's author account to enable or disable Post Actions.
- TypeScript
- GraphQL
First, enable or disable a specific Post Action invoking the corresponding enablePostAction or disablePostAction actions.
- TypeScript
- GraphQL
Then, handle the result using the adapter for the library of your choice:
The Lens SDK example here leverages a functional approach to chaining operations using the Result<T, E> object. See the Error Handling guide for more information.
See the Transaction Lifecycle guide for more information on how to determine the status of the transaction.
Building a Post Action
The Post Actions are defined by the IPostAction interface, which basically requires three functions, one to configure the action, one to execute it, and another to disable it.
interface IPostAction { function configure(address originalMsgSender, address feed, uint256 postId, KeyValue[] calldata params) external returns (bytes memory);
function execute(address originalMsgSender, address feed, uint256 postId, KeyValue[] calldata params) external returns (bytes memory);
function setDisabled( address originalMsgSender, address feed, uint256 postId, bool isDisabled, KeyValue[] calldata params ) external returns (bytes memory);}
But first, let's talk about the ActionHub
Before we dive into each of the functions from the IPostAction interface, we need to talk about the ActionHub.
The ActionHub is a special contract that acts as the entry point for every Action in the Lens Protocol.
The purpose of that is to help discovery of Actions, having a single point where all main Action-related events are emitted.
So, each function of the IPostAction interface must be only callable by ActionHub. For this, you can inherit BasePostAction contract, which acts as template for your custom Post Action, applying the restrictions needed, so you do not need to worry about it.
With this context, you can now understand why the first param of every IPostAction function is originalMsgSender: this is the address that called the ActionHub originally, either to configure, execute, or disable an Action, given that msg.sender will always have the ActionHub address in the context of your Action contract.
Configuration
The configuration of the Action is done through the configure function, which purpose is to initialize any required state that the Action might require to work properly.
The function receives four parameters:
originalMsgSender: the address of the original msg.sender that invoked the ActionHub (as explained above).
feed: the address of the Feed where the Post the Action is being configured for belongs to.
postId: the ID of the Post for which the Action is configured for.
params: array of key-value pairs whose values can be decoded into any extra custom configuration parameters that the Action could require to work.
Return of the configure function is bytes - some Actions might want to return custom information to the caller.
This configure function could be called by anyone (through the ActionHub, as explained above), it will depend on the implementation of the Action and its purpose who is allowed to invoke it (based on originalMsgSender).
For example, there might be Actions that do not require initialization at all, so the configure function implementation will be empty, while other Actions might require the caller to match the author of the given Post.
Keep in mind that if no prior configuration is required, the configure function must still be implemented and must not revert.
Every time the configure function is called, the ActionHub will emit a Lens_ActionHub_PostAction_Configured event matching the parameters of the call (or Lens_ActionHub_PostAction_Reconfigured if the configuration was updated).
Execution
The execution of the Action is done through the execute function, which purpose is to perform the actual action logic that the Action implements.
The function receives four parameters:
originalMsgSender: the address of the original msg.sender that invoked the ActionHub (as explained above).
feed: the address of the Feed where the Post the Action is being executed for belongs to.
postId: the ID of the Post for which the Action is being executed.
params: array of key-value pairs whose values can be decoded into any extra custom execution parameters that the Action could require to work.
Return of the execute function is bytes - some Actions might want to return custom information to the caller.
This execute function could be called by anyone (through the ActionHub, as explained above), it will depend on the implementation of the Action and its purpose who is allowed to invoke it (based on originalMsgSender).
For example, there might be Actions that do not require any permissions at all, so the execute function implementation will be open, while other Actions might require the caller to match the author of the given Post.
Every time the execute function is called, the ActionHub will emit a Lens_ActionHub_PostAction_Executed event matching the parameters of the call.
The ActionHub will not allow to invoke the execute function on a Post if the Action is disabled for it.
Disabling
The disabling of the Action is done through the setDisabled function, which purpose is to stop an Action to be executable for a given Post. The same function can be used to enable the Action back.
The function receives five parameters:
originalMsgSender: the address of the original msg.sender that invoked the ActionHub (as explained above).
feed: the address of the Feed where the Post the Action is being disabled/enabled for belongs to.
postId: the ID of the Post for which the Action is being disabled/enabled.
isDisabled: boolean indicating if the Action is being disabled or enabled.
params: array of key-value pairs whose values can be decoded into any extra custom disabling/enabling parameters that the Action could require to work.
Return of the setDisabled function is bytes - some Actions might want to return custom information to the caller.
This setDisabled function could be called by anyone (through the ActionHub, as explained above), it will depend on the implementation of the Action and its purpose who is allowed to invoke it (based on originalMsgSender).
Every time the setDisabled function is called, the ActionHub will emit a Lens_ActionHub_PostAction_Disabled event matching the parameters of the call.
Example
Let's illustrate the process with an example. We will build a Simple Poll Vote Action so users can vote (e.g., Yes/No) on a Post representing a poll. This action allows any user to vote once per post.
First, we define an event PollVoted to signal when a vote occurs and a mapping _hasVoted to track addresses that have already voted on a specific post to prevent double voting.
// SPDX-License-Identifier: MITpragma solidity ^0.8.20;
import { IPostAction } from "contracts/interfaces/IPostAction.sol";import { KeyValue } from "contracts/core/types/Types.sol";import { BasePostAction } from "contracts/actions/post/base/BasePostAction.sol";
contract SimplePollVoteAction is BasePostAction { event PollVoted(address indexed voter, uint256 indexed postId, bool vote);
mapping(address feed => mapping(uint256 postId => mapping(address voter => bool hasVoted))) private _hasVoted;
mapping(address feed => mapping(uint256 postId => mapping(bool vote => uint256 count))) private _voteCounts;
Next, implement the configure function. Although this simple poll action doesn't store complex configuration state on-chain (like the specific poll question), it's crucial for the post author to explicitly enable or "attach" this action to their post. This signals intent and allows UIs to discover that the post is intended as a poll.
We add a requirement that originalMsgSender (the address calling the ActionHub) must be the author of the postId on the given feed.
// ... inside SimplePollVoteAction ...
function _configure( address originalMsgSender, address feed, uint256 postId, KeyValue[] calldata params ) internal override returns (bytes memory) { require( originalMsgSender == IFeed(feed).getPostAuthor(postId), "Only author can configure" ); // Any extra configuration logic could be added here (e.g. mapping each possible vote type to some string) // Emitting an event Lens_ActionHub_PostAction_Configured happens automatically via ActionHub return ""; }
Implement the execute function. This is where the core voting logic resides.
First, we check if the originalMsgSender (the user initiating the action via the ActionHub) has already voted on this postId. If they have, the transaction reverts.
// ... inside SimplePollVoteAction ...
function _execute( address originalMsgSender, address feed, uint256 postId, KeyValue[] calldata params ) external override returns (bytes memory) { require(!_hasVoted[feed][postId][originalMsgSender], "Already voted");
_hasVoted[feed][postId][originalMsgSender] = true;
bool voteFound; bool vote; for (uint256 i = 0; i < params.length; i++) { if (params[i].key == keccak256("lens.param.vote")) { voteFound = true; vote = abi.decode(params[i].value, (bool)); break; } }
require(voteFound, "Vote not found in params");
_voteCounts[feed][postId][vote]++;
return abi.encode(vote); }
We can also add some getters for vote counts and then the SimplePollVoteAction is ready to be deployed and used.
See the full code below:
// SPDX-License-Identifier: MITpragma solidity ^0.8.20;
import { IPostAction } from "contracts/extensions/action/ActionHub.sol";import { KeyValue } from "contracts/core/types/Types.sol";import { BasePostAction } from "contracts/actions/post/base/BasePostAction.sol";import { IFeed } from "contracts/interfaces/IFeed.sol";
/** * @title SimplePollVoteAction * @notice A simple post action allowing users to cast a boolean vote (e.g., Yes/No) on a post. * Prevents double voting. */contract SimplePollVoteAction is BasePostAction { event PollVoted(address indexed voter, uint256 indexed postId, bool vote);
// feed => postId => voter => hasVoted mapping(address => mapping(uint256 => mapping(address => bool))) private _hasVoted;
/** * @notice Configures the SimplePollVote Action for a given post. * @param originalMsgSender The address initiating the configuration via the ActionHub. Must be post author. * @param feed The address of the feed contract where the post exists. * @param postId The ID of the post being configured. * @param params Not used * @return bytes Empty bytes. */ function _configure( address originalMsgSender, address feed, uint256 postId, KeyValue[] calldata params ) internal override returns (bytes memory) { require( originalMsgSender == IFeed(feed).getPostAuthor(postId), "Only author can configure" ); // Any extra configuration logic could be added here (e.g. mapping each possible vote type to some string) // Emitting an event Lens_ActionHub_PostAction_Configured happens automatically via ActionHub return ""; }
/** * @notice Executes a vote on a given post. * @param originalMsgSender The address initiating the vote via the ActionHub. * @param feed The address of the feed contract where the post exists. * @param postId The ID of the post being voted on. * @param params Array of key-value pairs. Expected to contain at least one element, * where the `value` of the first element is the ABI-encoded boolean vote. * @return bytes Empty bytes. * Requirements: * - The `originalMsgSender` must not have voted on this `postId` before. * - `params` must not be empty and the first element's value must be abi-decodable as a boolean. */ function _execute( address originalMsgSender, address feed, uint256 postId, KeyValue[] calldata params ) external override returns (bytes memory) { require(!_hasVoted[feed][postId][originalMsgSender], "Already voted");
_hasVoted[feed][postId][originalMsgSender] = true;
bool voteFound; bool vote; for (uint256 i = 0; i < params.length; i++) { if (params[i].key == keccak256("lens.param.vote")) { voteFound = true; vote = abi.decode(params[i].value, (bool)); break; } }
require(voteFound, "Vote not found in params");
_voteCounts[feed][postId][vote]++;
return abi.encode(vote); }
/** * @notice Gets the vote counts for a specific post. * @param feed The address of the feed contract where the post exists. * @param postId The ID of the post to get vote counts for. * @return (uint256, uint256) A tuple containing the counts for false and true votes respectively. */ function getVoteCounts(address feed, uint256 postId) external view returns (uint256 ya, uint256 nay) { return (_voteCounts[feed][postId][false], _voteCounts[feed][postId][true]); }}