magenxcommerce / module-auto-product-links
Automatically fills Magento's native Related Products, Up-Sells and Cross-Sells, plus a Frequently Bought Together link type of its own, from editable rules - with a default rule for each, so the whole catalog is linked on the first cron run. A rule pairs a source condition tree (which products it a
Package info
github.com/magenxcommerce/module-auto-product-links
Type:magento2-module
pkg:composer/magenxcommerce/module-auto-product-links
Requires
- php: ~8.3.0||~8.4.0||~8.5.0
- magento/framework: *
- magento/module-backend: *
- magento/module-catalog: *
- magento/module-catalog-graph-ql: *
- magento/module-catalog-rule: *
- magento/module-catalog-widget: *
- magento/module-config: *
- magento/module-cron: *
- magento/module-eav: *
- magento/module-graph-ql: *
- magento/module-related-product-graph-ql: *
- magento/module-rule: *
- magento/module-sales: *
- magento/module-store: *
- magento/module-ui: *
Requires (Dev)
None
Suggests
- magento/module-reports: Enables the default 'Best sellers first' ranking, which reads sales_bestsellers_aggregated_monthly. Without it that ranking degrades to the candidate pool's own order; every other ranking is unaffected.
Provides
None
Conflicts
None
Replaces
None
README
Built with AI assistance. This module is built with significant use of Claude Code, under active human review and guidance. It is open source and in active development. AI assistance does not lower the engineering bar: every change must meet the same correctness, security, documentation and maintainability requirements as any other contribution.
No warranty. We are not responsible for any incompatibility. By installing this software, you accept responsibility for its security, upgrades, patches and monitoring.
Fills Magento's native Related Products, Up-Sells and Cross-Sells, plus a Frequently Bought Together link type of its own, from editable rules, on a cron.
Related, up-sell and cross-sell links are written straight into catalog_product_link,
so the storefront reads them through the stock related_products / upsell_products /
crosssell_products GraphQL fields with no change at all.
Frequently Bought Together is deliberately not cross-sells. Cross-sells are the
merchant's add-on suggestions shown in the cart; Frequently Bought Together is what
customers actually bought in the same orders. The module registers its own link type
for it (magenx_bought_together in catalog_product_link_type) and exposes it as
ProductInterface.frequently_bought_together_products, resolved by Magento's own
linked-products batch resolver.
Out of the box
Installing the module seeds one active rule per link type, and the module is enabled with dry run off, so the first cron run links the whole catalog with nothing to set up:
| Rule | Storefront | Picks |
|---|---|---|
| Default: Related | "You May Also Like" | the cheapest products in the same category |
| Default: Up-Sells | "We Also Recommend" | the most expensive products in the same category |
| Default: Cross-Sells | cart | a random pick from the 50 cheapest products in the catalog |
| Default: Frequently Bought Together | "Frequently Bought Together" | products most often bought in the same orders |
They are ordinary rules in the rule grid, at priority 1000: edit them, switch them off, or add rules of your own beside them (see Priorities below).
"Same category" means the product's most specific categories: a shirt assigned to both Men and Men > Shirts is matched against Shirts only, so socks and coats from Men do not compete for its slots.
The default cross-sell rule chooses by Random products. The pick is stable: it is a hash of rule, product and candidate, so a product keeps the same cross-sells from night to night until the candidates change, and nothing is rewritten (or purged from cache) just because a run happened.
The guarantee
Links you add by hand are never moved, overwritten or deleted.
Every link the module writes gets a row in magenx_auto_link_ledger, and only links
with a ledger row are ever touched. A hand-picked link never gets one — including the
case where a rule independently computes a product you had already linked yourself,
which is subtracted out before anything is claimed. Manual links also keep their
positions (auto links are numbered from 1000 up, so yours sort first) and do not count
against a rule's link limit.
Every run logs manual_links_skipped, the number of times the module declined to claim
someone else's link. That is the standing audit trail for this promise.
What a rule is
| Part | Meaning |
|---|---|
| Fills | Related / Up-Sells / Cross-Sells / Frequently Bought Together |
| Apply this rule to these products | The source condition tree — which products get links |
| Link to these products | The target condition tree — the candidate pool |
| Choose products by | Similar products, Bought together or Random products |
| Must match the source product on | Attributes a candidate must share with the source (colour, size, manufacturer…), plus shares a category and price band |
| Price band (%) | Half-width of the band, e.g. 20 = within ±20% of the source's price |
| Maximum links per product | Cap on auto links (manual links are extra) |
| Show best | How the survivors are ranked before the cap |
| Randomly pick from the best | Optional N: link a stable random max links out of the best N instead of simply the best |
| Priority | Lower runs first; see below |
Random products links each product to a different random handful of the target pool — what cart cross-sells usually are. Match attributes and the price band still narrow the draw (e.g. random products from the same category), randomly pick from the best N limits it to the best N under Show best, and Show best decides the stored order of the picks.
Both condition trees are the stock Magento rule widget — the same attribute picker as a Catalog Price Rule, with any/all and is / is-not / greater-than.
Nothing is hardcoded per link type: a rule declares what it fills, so Bought together can drive Related Products just as well as Cross-Sells. The one restriction is the other way round: a Frequently Bought Together rule must choose by Bought together.
Priorities
Rules of one link type run in priority order, and each product has one owning rule per link type: the first rule that produces links for a product takes it, later rules skip it. A rule that finds nothing for a product leaves it to the next rule, which is how the priority-1000 defaults fill whatever your own rules (priority 0 by default) leave empty.
After all rules of a link type have run, auto links on products that no rule claimed any more - the product left a rule's conditions, or its rule was switched off - are removed. A rule that errors keeps its existing links rather than having them cleaned up.
Because ownership depends on the rules above it, Run Now runs every rule of the rule's link type, not the rule alone.
Example rules
- Related — Choose by Similar products, match on
colour+ shares a category, show best sellers first. - Up-Sells — Choose by Similar products, match on shares a category + price band,
price band
60, target treeprice greater than {…}, show most expensive first. - Cross-Sells — Choose by Random products, no match attributes, target tree
narrowed to an Accessories category, show cheapest first. Add randomly pick from the
best
30to draw only from the 30 cheapest accessories. - Frequently Bought Together — Choose by Bought together, show strongest match first.
Configuration
Stores → Configuration → Magenx → Auto Product Links (magenx_auto_product_links/…)
| Path | Default | Notes |
|---|---|---|
general/enabled |
1 |
Both crons return immediately when off |
general/dry_run |
0 |
Compute and log, write nothing |
general/invalidate_cache |
1 |
Broadcast cat_p_<id> after a run |
general/max_links_default |
12 |
Matches the storefront PDP rails: 12 per rail, 4 visible at a time |
general/auto_position_base |
1000 |
Auto links start here so manual links sort first |
limits/target_pool_cap |
200000 |
Memory guard. A rule whose target tree (empty = whole catalog) matches more is skipped and keeps its links |
limits/batch_size |
100 |
Source products per transaction |
copurchase/lookback_months |
6 |
|
copurchase/min_support |
2 |
Orders a pair must appear in |
copurchase/max_orders_per_run |
100000 |
|
copurchase/max_items_per_order |
50 |
Very large orders are skipped — see below |
cron/copurchase_schedule |
0 3 * * * |
|
cron/rules_schedule |
30 3 * * * |
Must run after the mining job |
Rules live at Marketing → Promotions → Auto Product Link Rules
(ACL Magenx_AutoProductLinks::rules).
Runs are logged to var/log/magenx_auto_product_links.log.
Deploy
bin/magento module:enable Magenx_AutoProductLinks
bin/magento setup:upgrade
bin/magento setup:di:compile # production mode
setup:upgrade registers the Frequently Bought Together link type and seeds the default
rules; the next magenx_auto_link_mine_copurchase / magenx_auto_link_apply_rules cron
runs do the rest. To link immediately, open any rule and press Run Now.
Upgrading from 1.0.x: enabled / dry_run defaults only apply where the admin never
saved them. If the section was saved with Enable off or Dry Run on, change them there.
Order matters: the mining job must run before the rule job, and the rule job should run after the catalog reindex so it works from current prices.
Caveats
- Product links are global.
catalog_product_linkhas no store, website or customer-group column, so a rule's store view setting only decides which store's attribute values, prices and orders are read — not who sees the resulting links. There is no way to give two store views different auto links through native links. Customer-group scoping is not offered for the same reason: it would silently write links every group sees. - Ranking may not reach the storefront. The order is written as the link
position, but Magento's stock GraphQL link resolvers do not guarantee position ordering in every 2.4.x patch. Verify against your own backend before relying on Show best. - New links can take hours to appear unless the purge chain is configured: Full Page
Cache in Varnish mode, a populated
http_cache_hosts, and an edge that forwardsPURGEto the storefront's/api/revalidate. Without it the storefront serves its own cached rails until they expire. - Very large orders are excluded from mining. The number of pairs an order contributes grows with the square of its line count, so one 200-line order would contribute ~39,800 pairs and drown ordinary baskets.
- Condition trees are pushed down to SQL, which is what makes a nightly whole-catalog pass viable. A condition on an attribute the SQL builder cannot map is skipped: inside an all/AND group that is harmless, but inside an any/OR group it can quietly match fewer products than expected.
- Frequently Bought Together needs orders. The first mining run backfills the whole look-back window, but a store with little order history produces few pairs, and a product with none gets no Frequently Bought Together rail.
- Links are counted at parent level. A configurable's parent product is what gets linked, since a link on an invisible variant renders nothing on the storefront.
Tables
| Table | Purpose |
|---|---|
magenx_auto_link_rule |
The rules, including both serialized condition trees |
magenx_auto_link_ledger |
Every link the module authored — the manual-link guarantee |
magenx_auto_link_copurchase |
Mined co-purchase pair counts, bucketed by month |
The list of already-mined months is kept in Magento's own flag table.
Extending
Add a way of choosing link targets by implementing
Magenx\AutoProductLinks\Api\TargetStrategyInterface and contributing it to the
strategies argument of Magenx\AutoProductLinks\Model\Strategy\StrategyPool in
di.xml. It then appears in the rule form and runs under every existing guard — link
caps, ordering, the ledger diff, dry run and cache invalidation — without touching the
runner.