> ## Documentation Index
> Fetch the complete documentation index at: https://docs.joinerywp.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Add your own conditions and actions

> Register rule types from your own plugin. They appear in the rules editor and work with every Joinery plugin.

Joinery Core fires `joinery_core_init` once it's ready. Register your rule types there with `joinery_register_condition()` and `joinery_register_action()`.

## A condition

A condition answers one question: does this rule apply? Its `scope()` says whether it looks at the whole cart (`Scope::CART`) or at one line (`Scope::ITEM`).

```php my-plugin/src/DifferentProducts.php theme={null}
<?php
use Joinery\Core\Rules\Condition;
use Joinery\Core\Rules\Context;
use Joinery\Core\Rules\Scope;

final class DifferentProducts implements Condition {
	public function id(): string {
		return 'acme_different_products'; // Prefix IDs with your plugin's prefix.
	}

	public function label(): string {
		return __( 'Different products in the cart', 'acme' );
	}

	public function scope(): string {
		return Scope::CART;
	}

	public function fields(): array {
		return array(
			array( 'key' => 'min', 'type' => 'number', 'label' => __( 'At least', 'acme' ), 'min' => 1 ),
		);
	}

	public function matches( array $settings, Context $context ): bool {
		$products = array_unique( array_column( $context->lines, 'product_id' ) );
		return count( $products ) >= max( 1, (int) ( $settings['min'] ?? 1 ) );
	}
}
```

Conditions must be pure: decide from `$settings` and `$context` only, with no database or WordPress calls, because rules are checked many times per request. If you need more about the customer, look at what `Context` already holds (such as `returning`, whether they've ordered before) before adding your own lookups.

## An action

Item actions implement `ItemPriceAction` and return a new unit price. Cart actions implement `CartDiscountAction` and return an amount to take off; it's spread over the cart lines by value. Offers that take money off particular lines implement `LineDiscountAction`: `line_discounts()` returns the amount off each line (keyed by its `key` in `$context->lines`) and `discount_label()` names the cart totals line, such as "Buy 2, get 1 free". The core takes VAT off with each discount at the rates of the lines it applies to.

```php my-plugin/src/RoundDown.php theme={null}
<?php
use Joinery\Core\Rules\Context;
use Joinery\Core\Rules\ItemPriceAction;

final class RoundDown implements ItemPriceAction {
	public function id(): string {
		return 'acme_round_down';
	}

	public function label(): string {
		return __( 'Round price down to .99', 'acme' );
	}

	public function fields(): array {
		return array();
	}

	public function adjust_price( float $price, array $settings, Context $context ): float {
		return max( 0.0, floor( $price ) - 0.01 );
	}
}
```

## An action that runs after the order

Event actions do something once an order is placed, such as giving store credit or adding a customer to a list. Implement `EventAction`: `events()` lists what it responds to, and `handle()` does the work. Gift Cards' "Give store credit" is one.

```php my-plugin/src/ThankYouTag.php theme={null}
<?php
use Joinery\Core\Rules\EventAction;
use Joinery\Core\Rules\EventRunner;

final class ThankYouTag implements EventAction {
	public function id(): string {
		return 'acme_thank_you_tag';
	}

	public function label(): string {
		return __( 'Tag the customer as a repeat buyer', 'acme' );
	}

	public function fields(): array {
		return array();
	}

	public function events(): array {
		return array( EventRunner::ORDER_COMPLETED );
	}

	public function handle( string $event, array $payload, array $settings ): void {
		if ( $payload['customer_id'] ) {
			update_user_meta( $payload['customer_id'], 'acme_repeat_buyer', 'yes' );
		}
	}
}
```

* **Events**: `EventRunner::ORDER_PAID` (`order.paid`, the first time the order reaches Processing or Completed) and `EventRunner::ORDER_COMPLETED` (`order.completed`).
* **When it runs**: in the background via Action Scheduler (group `joinery`), once per order and event. Conditions are checked against the order: its customer, the time it was placed, its subtotal and quantity, and a product condition matches when any line meets it.
* **Payload**: `order_id`, `customer_id` (0 for guests), `rule_id` and `rule_name`.
* Make `handle()` safe to run twice for the same order and rule, for example by recording what you did in order meta. Action Scheduler retries failed jobs.
* Rules that only have event actions never take part in pricing, so they can't stop a "Don't combine" pricing rule.

If your action's price changes at set quantities, also implement `Joinery\Core\Rules\QuantityBreaks`. Headless storefronts then get those quantities in [`quantity_breaks`](https://docs.joinerywp.com/headless/store-api#quantity-breaks-on-products).

## Register them

```php my-plugin/my-plugin.php theme={null}
<?php
/**
 * Plugin Name: Acme rules for Joinery
 * Requires Plugins: joinery-core
 */

add_action(
	'joinery_core_init',
	static function (): void {
		joinery_register_condition( new FirstOrder() );
		joinery_register_action( new RoundDown() );
	}
);
```

## Fields

`fields()` describes the settings form in the rules editor. Each field is an array with `key`, `type` and `label`. Supported types include `number`, `money`, `percent`, `select` (with `options`), `role-select`, `category-select`, `datetime` and `tiers`. Values arrive in `$settings` under each field's `key`. Add `help` to show a line of guidance under a number or select field.

> [!WARNING]
>
> Keep `matches()` and `adjust_price()` fast and free of side effects. They run for every cart line on every cart calculation. Side effects belong in an event action.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.