Configuring a Price-Drop Trigger for Viewed Products in 1C-Bitrix
A user has viewed a product several times without purchasing — a clear signal of interest. Automatically reducing the price 24–48 hours after the view, or issuing a personalised coupon, converts these users better than standard email campaigns. In Bitrix this is implemented via the marketing module and an event handler.
Tracking Views
Product views are written to an HL-block table. The standard statistics module (b_stat_page_event) is not suitable — we need a link between a specific user and a specific product:
// /local/lib/Tracking/ProductViewTracker.php
namespace Local\Tracking;
class ProductViewTracker
{
public static function track(int $userId, int $productId): void
{
if ($userId <= 0) return; // authenticated users only
$conn = \Bitrix\Main\Application::getConnection();
// Upsert: update view counter and last-viewed date
$conn->queryExecute("
INSERT INTO b_uts_product_view
(UF_USER_ID, UF_PRODUCT_ID, UF_VIEW_COUNT, UF_LAST_VIEW, UF_TRIGGER_SENT)
VALUES
({$userId}, {$productId}, 1, NOW(), 'N')
ON DUPLICATE KEY UPDATE
UF_VIEW_COUNT = UF_VIEW_COUNT + 1,
UF_LAST_VIEW = NOW()
");
}
}
Call this in the product card component, inside result_modifier.php:
// /local/templates/.default/components/bitrix/catalog.element/main/result_modifier.php
global $USER;
if ($USER->IsAuthorized()) {
\Local\Tracking\ProductViewTracker::track(
(int)$USER->GetID(),
(int)$arResult['ID']
);
}
Trigger Agent
The agent runs once an hour, finds users meeting the qualifying criteria, and applies a discount:
namespace Local\Marketing;
class PriceDropTriggerAgent
{
public static function run(): string
{
$conn = \Bitrix\Main\Application::getConnection();
// Products viewed 2+ times, not purchased, last viewed 24+ hours ago
$candidates = $conn->query("
SELECT pv.UF_USER_ID, pv.UF_PRODUCT_ID, pv.UF_VIEW_COUNT
FROM b_uts_product_view pv
LEFT JOIN b_sale_basket sb
ON sb.USER_ID = pv.UF_USER_ID
AND sb.PRODUCT_ID = pv.UF_PRODUCT_ID
AND sb.ORDER_ID IS NOT NULL
WHERE pv.UF_VIEW_COUNT >= 2
AND pv.UF_LAST_VIEW < DATE_SUB(NOW(), INTERVAL 24 HOUR)
AND pv.UF_TRIGGER_SENT = 'N'
AND sb.ID IS NULL
LIMIT 50
");
while ($row = $candidates->fetch()) {
self::applyDiscount($row['UF_USER_ID'], $row['UF_PRODUCT_ID']);
// Mark to prevent re-application
$conn->queryExecute("
UPDATE b_uts_product_view
SET UF_TRIGGER_SENT = 'Y'
WHERE UF_USER_ID = {$row['UF_USER_ID']}
AND UF_PRODUCT_ID = {$row['UF_PRODUCT_ID']}
");
}
return '\Local\Marketing\PriceDropTriggerAgent::run();';
}
private static function applyDiscount(int $userId, int $productId): void
{
// Create a personalised coupon via the marketing module
$couponCode = 'VIEW_' . strtoupper(substr(md5($userId . $productId . time()), 0, 8));
\CCatalogDiscountCoupon::Add([
'DISCOUNT_ID' => VIEWED_PRODUCT_DISCOUNT_ID, // ID of the pre-created 10% discount
'CODE' => $couponCode,
'ONE_TIME' => 'Y', // single-use
'ACTIVE' => 'Y',
'ACTIVE_FROM' => new \Bitrix\Main\Type\DateTime(),
'ACTIVE_TO' => \Bitrix\Main\Type\DateTime::createFromTimestamp(time() + 86400 * 3),
'MAX_USE' => 1,
]);
// Associate the coupon with the user via HL-block
PersonalCouponRepository::save($userId, $productId, $couponCode);
// Email the user
self::sendEmail($userId, $productId, $couponCode);
}
}
Discount via the Marketing Module
The base discount is created once through the admin interface (Marketing → Discounts) or via API:
\CCatalogDiscount::Add([
'NAME' => 'Personal discount on a viewed product',
'LID' => SITE_ID,
'ACTIVE' => 'Y',
'VALUE' => 10, // 10% discount
'VALUE_TYPE' => 'P',
'COUPON_TYPE' => 'U', // coupon required
'SORT' => 300,
'PRIORITY' => 1,
]);
The ID of this discount is assigned to the constant VIEWED_PRODUCT_DISCOUNT_ID.
Data Retention
View records are purged by an agent once a week: records older than 30 days with the trigger already sent are deleted. The table does not grow to an unmanageable size.
| Configuration | Timeline |
|---|---|
| Tracking + agent + email + coupon | 3–5 days |
| + trigger conversion analytics | +2 days |







