> For AI agents: the complete documentation index is available at https://nhanbe.id.vn/llms.txt, the full documentation bundle is available at https://nhanbe.id.vn/llms-full.txt.

# Sling Jobs and Scheduler

- **Sling Jobs** - guaranteed, persistent, cluster-aware task execution with automatic retries
- **Schedulers** - time-based triggers (cron, periodic) that execute code on a schedule

Together they handle everything from cache warming and report generation to integration syncs and content cleanup.

<svg id="mermaid-svg-8460699" width="100%" xmlns="http://www.w3.org/2000/svg" style="max-width: 1327.135498046875px;" viewBox="0 0 1327.135498046875 169" role="graphics-document document" aria-roledescription="flowchart-v2"><g><marker id="mermaid-svg-8460699_flowchart-v2-pointEnd" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker><marker id="mermaid-svg-8460699_flowchart-v2-pointStart" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker><marker id="mermaid-svg-8460699_flowchart-v2-pointEnd-margin" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" style="stroke-width: 0; stroke-dasharray: 1, 0;"></path></marker><marker id="mermaid-svg-8460699_flowchart-v2-pointStart-margin" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" style="stroke-width: 0; stroke-dasharray: 1, 0;"></polygon></marker><marker id="mermaid-svg-8460699_flowchart-v2-circleEnd" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" style="stroke-width: 1; stroke-dasharray: 1, 0;"></circle></marker><marker id="mermaid-svg-8460699_flowchart-v2-circleStart" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" style="stroke-width: 1; stroke-dasharray: 1, 0;"></circle></marker><marker id="mermaid-svg-8460699_flowchart-v2-circleEnd-margin" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" style="stroke-width: 0; stroke-dasharray: 1, 0;"></circle></marker><marker id="mermaid-svg-8460699_flowchart-v2-circleStart-margin" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" style="stroke-width: 0; stroke-dasharray: 1, 0;"></circle></marker><marker id="mermaid-svg-8460699_flowchart-v2-crossEnd" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" style="stroke-width: 2; stroke-dasharray: 1, 0;"></path></marker><marker id="mermaid-svg-8460699_flowchart-v2-crossStart" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" style="stroke-width: 2; stroke-dasharray: 1, 0;"></path></marker><marker id="mermaid-svg-8460699_flowchart-v2-crossEnd-margin" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" style="stroke-width: 2.5;"></path></marker><marker id="mermaid-svg-8460699_flowchart-v2-crossStart-margin" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"></path></marker><g><g></g><g><path d="M268,92L272.167,92C276.333,92,284.667,92,292.333,92C300,92,307,92,310.5,92L314,92" id="mermaid-svg-8460699-L_Trigger_JM_0" style=";" data-edge="true" data-et="edge" data-id="L_Trigger_JM_0" data-points="W3sieCI6MjY4LCJ5Ijo5Mn0seyJ4IjoyOTMsInkiOjkyfSx7IngiOjMxOCwieSI6OTJ9XQ==" data-look="classic" marker-end="url(#mermaid-svg-8460699_flowchart-v2-pointEnd)" fill="none" stroke="currentColor"></path><path d="M529.865,92L534.031,92C538.198,92,546.531,92,554.198,92C561.865,92,568.865,92,572.365,92L575.865,92" id="mermaid-svg-8460699-L_JM_Queue_0" style=";" data-edge="true" data-et="edge" data-id="L_JM_Queue_0" data-points="W3sieCI6NTI5Ljg2NDU3ODI0NzA3MDMsInkiOjkyfSx7IngiOjU1NC44NjQ1NzgyNDcwNzAzLCJ5Ijo5Mn0seyJ4Ijo1NzkuODY0NTc4MjQ3MDcwMywieSI6OTJ9XQ==" data-look="classic" marker-end="url(#mermaid-svg-8460699_flowchart-v2-pointEnd)" fill="none" stroke="currentColor"></path><path d="M799.539,55L804.393,53.333C809.248,51.667,818.957,48.333,827.312,46.667C835.667,45,842.667,45,846.167,45L849.667,45" id="mermaid-svg-8460699-L_Queue_Consumer_0" style=";" data-edge="true" data-et="edge" data-id="L_Queue_Consumer_0" data-points="W3sieCI6Nzk5LjUzODc3NTgyOTU1ODYsInkiOjU1fSx7IngiOjgyOC42NjY2NTY0OTQxNDA2LCJ5Ijo0NX0seyJ4Ijo4NTMuNjY2NjU2NDk0MTQwNiwieSI6NDV9XQ==" data-look="classic" marker-end="url(#mermaid-svg-8460699_flowchart-v2-pointEnd)" fill="none" stroke="currentColor"></path><path d="M1009.135,45L1013.302,45C1017.469,45,1025.802,45,1033.497,46.07C1041.193,47.14,1048.25,49.28,1051.779,50.35L1055.308,51.42" id="mermaid-svg-8460699-L_Consumer_Result_0" style=";" data-edge="true" data-et="edge" data-id="L_Consumer_Result_0" data-points="W3sieCI6MTAwOS4xMzU0MDY0OTQxNDA2LCJ5Ijo0NX0seyJ4IjoxMDM0LjEzNTQwNjQ5NDE0MDYsInkiOjQ1fSx7IngiOjEwNTkuMTM1NDA2NDk0MTQwNiwieSI6NTIuNTgwNjQ1MTYxMjkwMzJ9XQ==" data-look="classic" marker-end="url(#mermaid-svg-8460699_flowchart-v2-pointEnd)" fill="none" stroke="currentColor"></path><path d="M1059.135,131.419L1054.969,132.683C1050.802,133.946,1042.469,136.473,1021.18,137.737C999.891,139,965.646,139,931.401,139C897.156,139,862.911,139,841.565,137.55C820.218,136.1,811.77,133.199,807.546,131.749L803.322,130.299" id="mermaid-svg-8460699-L_Result_Queue_0" style=";" data-edge="true" data-et="edge" data-id="L_Result_Queue_0" data-points="W3sieCI6MTA1OS4xMzU0MDY0OTQxNDA2LCJ5IjoxMzEuNDE5MzU0ODM4NzA5N30seyJ4IjoxMDM0LjEzNTQwNjQ5NDE0MDYsInkiOjEzOX0seyJ4Ijo5MzEuNDAxMDMxNDk0MTQwNiwieSI6MTM5fSx7IngiOjgyOC42NjY2NTY0OTQxNDA2LCJ5IjoxMzl9LHsieCI6Nzk5LjUzODc3NTgyOTU1ODYsInkiOjEyOX1d" data-look="classic" marker-end="url(#mermaid-svg-8460699_flowchart-v2-pointEnd)" fill="none" stroke="currentColor"></path></g><g><g><g data-id="L_Trigger_JM_0" transform="translate(0, 0)"></g></g><g><g data-id="L_JM_Queue_0" transform="translate(0, 0)"></g></g><g><g data-id="L_Queue_Consumer_0" transform="translate(0, 0)"></g></g><g><g data-id="L_Consumer_Result_0" transform="translate(0, 0)"></g></g><g transform="translate(931.4010314941406, 139)"><g data-id="L_Result_Queue_0" transform="translate(-71.453125, -22)"><foreignObject width="142.90625" height="44"><p>FAILED + retries left</p></foreignObject></g></g></g><g><g id="mermaid-svg-8460699-flowchart-Trigger-0" data-look="classic" transform="translate(138, 92)"><rect style="" x="-130" y="-49" width="260" height="98" fill="none" stroke="currentColor"></rect><g style="" transform="translate(-100, -34)"><rect></rect><foreignObject width="200" height="68"><p>Trigger (code, event, scheduler)</p></foreignObject></g></g><g id="mermaid-svg-8460699-flowchart-JM-1" data-look="classic" transform="translate(423.93228912353516, 92)"><rect style="" x="-105.93229675292969" y="-37" width="211.86459350585938" height="74" fill="none" stroke="currentColor"></rect><g style="" transform="translate(-75.93229675292969, -22)"><rect></rect><foreignObject width="151.86459350585938" height="44"><p>JobManager.addJob()</p></foreignObject></g></g><g id="mermaid-svg-8460699-flowchart-Queue-2" data-look="classic" transform="translate(691.7656173706055, 92)"><rect style="" x="-111.90104675292969" y="-37" width="223.80209350585938" height="74" fill="none" stroke="currentColor"></rect><g style="" transform="translate(-81.90104675292969, -22)"><rect></rect><foreignObject width="163.80209350585938" height="44"><p>Job Queue (persistent)</p></foreignObject></g></g><g id="mermaid-svg-8460699-flowchart-Consumer-3" data-look="classic" transform="translate(931.4010314941406, 45)"><rect style="" x="-77.734375" y="-37" width="155.46875" height="74" fill="none" stroke="currentColor"></rect><g style="" transform="translate(-47.734375, -22)"><rect></rect><foreignObject width="95.46875" height="44"><p>JobConsumer</p></foreignObject></g></g><g id="mermaid-svg-8460699-flowchart-Result-4" data-look="classic" transform="translate(1189.1354064941406, 92)"><rect style="" x="-130" y="-49" width="260" height="98" fill="none" stroke="currentColor"></rect><g style="" transform="translate(-100, -34)"><rect></rect><foreignObject width="200" height="68"><p>Result (OK / FAILED / CANCEL)</p></foreignObject></g></g></g></g></g><defs></defs><defs></defs><linearGradient id="mermaid-svg-8460699-gradient" gradientUnits="objectBoundingBox" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#cccccc" stop-opacity="1"></stop><stop offset="100%" stop-color="hsl(180, 0%, 18.3529411765%)" stop-opacity="1"></stop></linearGradient></svg>

***

## Sling Jobs

### Why Sling Jobs?

| Feature                  | Sling Jobs             | Direct execution | EventHandler              |
| ------------------------ | ---------------------- | ---------------- | ------------------------- |
| **Guaranteed execution** | Yes (persisted to JCR) | No               | No                        |
| **Survives restart**     | Yes                    | No               | No                        |
| **Automatic retries**    | Yes (configurable)     | No               | No                        |
| **Cluster-aware**        | Yes (runs on one node) | No               | All nodes                 |
| **Monitoring**           | Felix console, JMX     | None             | None                      |
| **Async**                | Always                 | Depends          | Async but fire-and-forget |

Use Sling Jobs whenever failure of the operation would mean **data loss** or an **inconsistent state** - e.g., sending emails, replication, third-party API calls, report generation.

### Architecture

<svg id="mermaid-svg-2955633" width="100%" xmlns="http://www.w3.org/2000/svg" style="max-width: 645.875px;" viewBox="0 0 645.875 808" role="graphics-document document" aria-roledescription="flowchart-v2"><g><marker id="mermaid-svg-2955633_flowchart-v2-pointEnd" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker><marker id="mermaid-svg-2955633_flowchart-v2-pointStart" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker><marker id="mermaid-svg-2955633_flowchart-v2-pointEnd-margin" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" style="stroke-width: 0; stroke-dasharray: 1, 0;"></path></marker><marker id="mermaid-svg-2955633_flowchart-v2-pointStart-margin" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" style="stroke-width: 0; stroke-dasharray: 1, 0;"></polygon></marker><marker id="mermaid-svg-2955633_flowchart-v2-circleEnd" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" style="stroke-width: 1; stroke-dasharray: 1, 0;"></circle></marker><marker id="mermaid-svg-2955633_flowchart-v2-circleStart" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" style="stroke-width: 1; stroke-dasharray: 1, 0;"></circle></marker><marker id="mermaid-svg-2955633_flowchart-v2-circleEnd-margin" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" style="stroke-width: 0; stroke-dasharray: 1, 0;"></circle></marker><marker id="mermaid-svg-2955633_flowchart-v2-circleStart-margin" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" style="stroke-width: 0; stroke-dasharray: 1, 0;"></circle></marker><marker id="mermaid-svg-2955633_flowchart-v2-crossEnd" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" style="stroke-width: 2; stroke-dasharray: 1, 0;"></path></marker><marker id="mermaid-svg-2955633_flowchart-v2-crossStart" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" style="stroke-width: 2; stroke-dasharray: 1, 0;"></path></marker><marker id="mermaid-svg-2955633_flowchart-v2-crossEnd-margin" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" style="stroke-width: 2.5;"></path></marker><marker id="mermaid-svg-2955633_flowchart-v2-crossStart-margin" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"></path></marker><g><g><g id="mermaid-svg-2955633-subGraph2" data-look="classic"><rect style="" x="32.989585876464844" y="676" width="594.9947891235352" height="124"></rect><g transform="translate(242.45573043823242, 676)"><foreignObject width="176.0625" height="44"><p>Consumer (OSGi service)</p></foreignObject></g></g><g id="mermaid-svg-2955633-subGraph1" data-look="classic"><rect style="" x="8" y="206" width="629.875" height="420"></rect><g transform="translate(251.40625, 206)"><foreignObject width="143.0625" height="44"><p>Sling Job Subsystem</p></foreignObject></g></g><g id="mermaid-svg-2955633-subGraph0" data-look="classic"><rect style="" x="156.25" y="8" width="330" height="148"></rect><g transform="translate(221.7083282470703, 8)"><foreignObject width="199.08334350585938" height="44"><p>Producer (any OSGi service)</p></foreignObject></g></g></g><g><path d="M321.25,131L321.25,135.167C321.25,139.333,321.25,147.667,321.25,156C321.25,164.333,321.25,172.667,321.25,181C321.25,189.333,321.25,197.667,321.25,205.333C321.25,213,321.25,220,321.25,223.5L321.25,227" id="mermaid-svg-2955633-L_Add_Store_0" style=";" data-edge="true" data-et="edge" data-id="L_Add_Store_0" data-points="W3sieCI6MzIxLjI1LCJ5IjoxMzF9LHsieCI6MzIxLjI1LCJ5IjoxNTZ9LHsieCI6MzIxLjI1LCJ5IjoxODF9LHsieCI6MzIxLjI1LCJ5IjoyMDZ9LHsieCI6MzIxLjI1LCJ5IjoyMzF9XQ==" data-look="classic" marker-end="url(#mermaid-svg-2955633_flowchart-v2-pointEnd)" fill="none" stroke="currentColor"></path><path d="M321.25,329L321.25,333.167C321.25,337.333,321.25,345.667,321.25,353.333C321.25,361,321.25,368,321.25,371.5L321.25,375" id="mermaid-svg-2955633-L_Store_QueueManager_0" style=";" data-edge="true" data-et="edge" data-id="L_Store_QueueManager_0" data-points="W3sieCI6MzIxLjI1LCJ5IjozMjl9LHsieCI6MzIxLjI1LCJ5IjozNTR9LHsieCI6MzIxLjI1LCJ5IjozNzl9XQ==" data-look="classic" marker-end="url(#mermaid-svg-2955633_flowchart-v2-pointEnd)" fill="none" stroke="currentColor"></path><path d="M236.021,450.851L224.955,455.375C213.889,459.9,191.757,468.95,180.691,478.975C169.625,489,169.625,500,169.625,505.5L169.625,511" id="mermaid-svg-2955633-L_QueueManager_Queue1_0" style=";" data-edge="true" data-et="edge" data-id="L_QueueManager_Queue1_0" data-points="W3sieCI6MjM2LjAyMDgzNTg3NjQ2NDg0LCJ5Ijo0NTAuODUwNTA3MzQxNTI4fSx7IngiOjE2OS42MjUsInkiOjQ3OH0seyJ4IjoxNjkuNjI1LCJ5Ijo1MTV9XQ==" data-look="classic" marker-end="url(#mermaid-svg-2955633_flowchart-v2-pointEnd)" fill="none" stroke="currentColor"></path><path d="M406.479,450.851L417.545,455.375C428.611,459.9,450.743,468.95,461.809,476.975C472.875,485,472.875,492,472.875,495.5L472.875,499" id="mermaid-svg-2955633-L_QueueManager_Queue2_0" style=";" data-edge="true" data-et="edge" data-id="L_QueueManager_Queue2_0" data-points="W3sieCI6NDA2LjQ3OTE2NDEyMzUzNTE2LCJ5Ijo0NTAuODUwNTA3MzQxNTI4fSx7IngiOjQ3Mi44NzUsInkiOjQ3OH0seyJ4Ijo0NzIuODc1LCJ5Ijo1MDN9XQ==" data-look="classic" marker-end="url(#mermaid-svg-2955633_flowchart-v2-pointEnd)" fill="none" stroke="currentColor"></path><path d="M169.625,589L169.625,595.167C169.625,601.333,169.625,613.667,169.625,624C169.625,634.333,169.625,642.667,169.625,651C169.625,659.333,169.625,667.667,169.625,675.333C169.625,683,169.625,690,169.625,693.5L169.625,697" id="mermaid-svg-2955633-L_Queue1_Consumer1_0" style=";" data-edge="true" data-et="edge" data-id="L_Queue1_Consumer1_0" data-points="W3sieCI6MTY5LjYyNSwieSI6NTg5fSx7IngiOjE2OS42MjUsInkiOjYyNn0seyJ4IjoxNjkuNjI1LCJ5Ijo2NTF9LHsieCI6MTY5LjYyNSwieSI6Njc2fSx7IngiOjE2OS42MjUsInkiOjcwMX1d" data-look="classic" marker-end="url(#mermaid-svg-2955633_flowchart-v2-pointEnd)" fill="none" stroke="currentColor"></path><path d="M472.875,601L472.875,605.167C472.875,609.333,472.875,617.667,472.875,626C472.875,634.333,472.875,642.667,472.875,651C472.875,659.333,472.875,667.667,472.875,675.333C472.875,683,472.875,690,472.875,693.5L472.875,697" id="mermaid-svg-2955633-L_Queue2_Consumer2_0" style=";" data-edge="true" data-et="edge" data-id="L_Queue2_Consumer2_0" data-points="W3sieCI6NDcyLjg3NSwieSI6NjAxfSx7IngiOjQ3Mi44NzUsInkiOjYyNn0seyJ4Ijo0NzIuODc1LCJ5Ijo2NTF9LHsieCI6NDcyLjg3NSwieSI6Njc2fSx7IngiOjQ3Mi44NzUsInkiOjcwMX1d" data-look="classic" marker-end="url(#mermaid-svg-2955633_flowchart-v2-pointEnd)" fill="none" stroke="currentColor"></path></g><g><g><g data-id="L_Add_Store_0" transform="translate(0, 0)"></g></g><g><g data-id="L_Store_QueueManager_0" transform="translate(0, 0)"></g></g><g><g data-id="L_QueueManager_Queue1_0" transform="translate(0, 0)"></g></g><g><g data-id="L_QueueManager_Queue2_0" transform="translate(0, 0)"></g></g><g><g data-id="L_Queue1_Consumer1_0" transform="translate(0, 0)"></g></g><g><g data-id="L_Queue2_Consumer2_0" transform="translate(0, 0)"></g></g></g><g><g id="mermaid-svg-2955633-flowchart-Add-0" data-look="classic" transform="translate(321.25, 82)"><rect style="" x="-130" y="-49" width="260" height="98" fill="none" stroke="currentColor"></rect><g style="" transform="translate(-100, -34)"><rect></rect><foreignObject width="200" height="68"><p>jobManager.addJob(topic, props)</p></foreignObject></g></g><g id="mermaid-svg-2955633-flowchart-Store-1" data-look="classic" transform="translate(321.25, 280)"><rect style="" x="-130" y="-49" width="260" height="98" fill="none" stroke="currentColor"></rect><g style="" transform="translate(-100, -34)"><rect></rect><foreignObject width="200" height="68"><p>Job Store (/var/eventing/jobs)</p></foreignObject></g></g><g id="mermaid-svg-2955633-flowchart-QueueManager-2" data-look="classic" transform="translate(321.25, 416)"><rect style="" x="-85.22916793823242" y="-37" width="170.45833587646484" height="74" fill="none" stroke="currentColor"></rect><g style="" transform="translate(-55.22916793823242, -22)"><rect></rect><foreignObject width="110.45833587646484" height="44"><p>Queue Manager</p></foreignObject></g></g><g id="mermaid-svg-2955633-flowchart-Queue1-3" data-look="classic" transform="translate(169.625, 552)"><rect style="" x="-123.25" y="-37" width="246.5" height="74" fill="none" stroke="currentColor"></rect><g style="" transform="translate(-93.25, -22)"><rect></rect><foreignObject width="186.5" height="44"><p>Queue: myproject/import</p></foreignObject></g></g><g id="mermaid-svg-2955633-flowchart-Queue2-4" data-look="classic" transform="translate(472.875, 552)"><rect style="" x="-130" y="-49" width="260" height="98" fill="none" stroke="currentColor"></rect><g style="" transform="translate(-100, -34)"><rect></rect><foreignObject width="200" height="68"><p>Queue: myproject/notification</p></foreignObject></g></g><g id="mermaid-svg-2955633-flowchart-Consumer1-5" data-look="classic" transform="translate(169.625, 738)"><rect style="" x="-101.63542175292969" y="-37" width="203.27084350585938" height="74" fill="none" stroke="currentColor"></rect><g style="" transform="translate(-71.63542175292969, -22)"><rect></rect><foreignObject width="143.27084350585938" height="44"><p>ImportJobConsumer</p></foreignObject></g></g><g id="mermaid-svg-2955633-flowchart-Consumer2-6" data-look="classic" transform="translate(472.875, 738)"><rect style="" x="-120.109375" y="-37" width="240.21875" height="74" fill="none" stroke="currentColor"></rect><g style="" transform="translate(-90.109375, -22)"><rect></rect><foreignObject width="180.21875" height="44"><p>NotificationJobConsumer</p></foreignObject></g></g></g></g></g><defs></defs><defs></defs><linearGradient id="mermaid-svg-2955633-gradient" gradientUnits="objectBoundingBox" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#cccccc" stop-opacity="1"></stop><stop offset="100%" stop-color="hsl(180, 0%, 18.3529411765%)" stop-opacity="1"></stop></linearGradient></svg>

Jobs are stored in the JCR under `/var/eventing/jobs/` and survive restarts. The Queue Manager assigns jobs to consumers based on the **job topic**.

***

### Creating Jobs

Inject `JobManager` and call `addJob()`:

```java
import org.apache.sling.event.jobs.Job;
import org.apache.sling.event.jobs.JobManager;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;

import java.util.HashMap;
import java.util.Map;

@Component(service = AssetImportService.class)
public class AssetImportService {

    // Job topics follow a reverse-domain naming convention
    public static final String JOB_TOPIC = "com/myproject/jobs/asset-import";

    @Reference
    private JobManager jobManager;

    public void scheduleImport(String sourceUrl, String targetPath) {
        Map<String, Object> properties = new HashMap<>();
        properties.put("sourceUrl", sourceUrl);
        properties.put("targetPath", targetPath);
        properties.put("requestedBy", "admin");
        properties.put("timestamp", System.currentTimeMillis());

        Job job = jobManager.addJob(JOB_TOPIC, properties);

        if (job != null) {
            // Job was accepted and queued
            log.info("Import job created: {}", job.getId());
        }
    }
}
```

#### Job properties

Job properties are stored as a `Map<String, Object>`. Supported value types: `String`, `Boolean`, `Integer`, `Long`, `Double`, `String[]`, `Calendar`.

> Do **not** store large objects (binary data, entire documents) in job properties. Store a reference path or ID instead.

***

### Consuming Jobs

Implement `JobConsumer` and register it for a topic:

```java
import org.apache.sling.event.jobs.Job;
import org.apache.sling.event.jobs.consumer.JobConsumer;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Component(
    service = JobConsumer.class,
    property = {
        JobConsumer.PROPERTY_TOPICS + "=" + AssetImportService.JOB_TOPIC
    }
)
public class AssetImportJobConsumer implements JobConsumer {

    private static final Logger LOG = LoggerFactory.getLogger(AssetImportJobConsumer.class);

    @Reference
    private ResourceResolverFactory resolverFactory;

    @Override
    public JobResult process(Job job) {
        String sourceUrl = job.getProperty("sourceUrl", String.class);
        String targetPath = job.getProperty("targetPath", String.class);

        LOG.info("Processing asset import: {} → {}", sourceUrl, targetPath);

        try {
            // Perform the actual work
            importAsset(sourceUrl, targetPath);

            LOG.info("Import completed successfully");
            return JobResult.OK;

        } catch (TransientException e) {
            // Temporary failure (network issue, timeout) - retry
            LOG.warn("Transient failure, will retry: {}", e.getMessage());
            return JobResult.FAILED;

        } catch (Exception e) {
            // Permanent failure - do not retry
            LOG.error("Permanent failure, cancelling job: {}", e.getMessage(), e);
            return JobResult.CANCEL;
        }
    }

    private void importAsset(String sourceUrl, String targetPath) throws Exception {
        // Implementation here
    }
}
```

### JobResult values

| Result             | Behaviour                                                        |
| ------------------ | ---------------------------------------------------------------- |
| `JobResult.OK`     | Job completed successfully; removed from queue                   |
| `JobResult.FAILED` | Job failed; will be retried if retries remain                    |
| `JobResult.CANCEL` | Job failed permanently; removed from queue, no retry             |
| `JobResult.ASYNC`  | Job is processing asynchronously; call `job.acknowledge()` later |

***

### JobExecutor (alternative)

`JobExecutor` is a more modern alternative to `JobConsumer` that provides an `JobExecutionContext` for progress reporting and cancellation:

```java
import org.apache.sling.event.jobs.Job;
import org.apache.sling.event.jobs.consumer.JobExecutor;
import org.apache.sling.event.jobs.consumer.JobExecutionContext;
import org.apache.sling.event.jobs.consumer.JobExecutionResult;

@Component(
    service = JobExecutor.class,
    property = {
        JobExecutor.PROPERTY_TOPICS + "=" + "com/myproject/jobs/bulk-update"
    }
)
public class BulkUpdateJobExecutor implements JobExecutor {

    @Override
    public JobExecutionResult process(Job job, JobExecutionContext context) {
        int totalItems = job.getProperty("totalItems", 100);

        context.initProgress(totalItems, -1);

        for (int i = 0; i < totalItems; i++) {
            // Check if the job was cancelled
            if (context.isStopped()) {
                return context.result().message("Job was stopped").cancelled();
            }

            // Process item
            processItem(i);
            context.incrementProgressCount(1);
            context.log("Processed item {0} of {1}", i + 1, totalItems);
        }

        return context.result().message("All items processed").succeeded();
    }
}
```

***

### Queue Configuration

Job queues control concurrency, ordering, and retry behaviour. Configure them via OSGi:

```json
{
    "queue.name": "Asset Import Queue",
    "queue.topics": ["com/myproject/jobs/asset-import"],
    "queue.type": "ORDERED",
    "queue.maxparallel": 1,
    "queue.retries": 5,
    "queue.retrydelay": 30000,
    "queue.priority": "NORM",
    "queue.keepJobs": false
}
```

#### Queue types

| Type                | Behaviour                                                                         |
| ------------------- | --------------------------------------------------------------------------------- |
| `ORDERED`           | Jobs processed one at a time, in order; if one fails, the queue pauses            |
| `UNORDERED`         | Jobs processed in parallel (up to `maxparallel`); failures don't block other jobs |
| `TOPIC_ROUND_ROBIN` | Distributes jobs across cluster nodes in round-robin fashion                      |

#### Key properties

| Property                            | Default | Description                                        |
| ----------------------------------- | ------- | -------------------------------------------------- |
| `queue.retries`                     | 10      | Max retry attempts before giving up                |
| `queue.retrydelay`                  | 2000    | Delay between retries (ms)                         |
| `queue.maxparallel`                 | 15      | Max concurrent job processing threads              |
| `queue.keepJobs`                    | false   | Keep finished jobs in the JCR (for auditing)       |
| `queue.preferRunOnCreationInstance` | false   | Prefer running the job on the node that created it |

***

## Schedulers

Schedulers execute code at specific times or intervals. AEM supports two patterns.

### Pattern 1: OSGi Scheduler service (recommended)

Register a `Runnable` as an OSGi service with scheduler properties:

```java
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Component(
    service = Runnable.class,
    immediate = true
)
@Designate(ocd = CacheWarmerScheduler.Config.class)
public class CacheWarmerScheduler implements Runnable {

    private static final Logger LOG = LoggerFactory.getLogger(CacheWarmerScheduler.class);

    @ObjectClassDefinition(name = "Cache Warmer Scheduler")
    @interface Config {

        @AttributeDefinition(name = "Cron expression",
            description = "Quartz cron expression. Default: every hour")
        String scheduler_expression() default "0 0 * * * ?";

        @AttributeDefinition(name = "Concurrent",
            description = "Allow concurrent execution")
        boolean scheduler_concurrent() default false;

        @AttributeDefinition(name = "Enabled")
        boolean enabled() default true;

        @AttributeDefinition(name = "Paths to warm")
        String[] paths() default {"/content/mysite/en"};
    }

    private Config config;

    @Activate
    @Modified
    protected void activate(Config config) {
        this.config = config;
    }

    @Override
    public void run() {
        if (!config.enabled()) {
            return;
        }

        LOG.info("Cache warmer running for {} paths", config.paths().length);

        for (String path : config.paths()) {
            warmCache(path);
        }
    }

    private void warmCache(String path) {
        // Fetch pages to warm the Dispatcher cache
        LOG.debug("Warming cache for: {}", path);
    }
}
```

OSGi config to deploy:

```json
{
    "scheduler.expression": "0 0 */2 * * ?",
    "scheduler.concurrent": false,
    "enabled": true,
    "paths": ["/content/mysite/en", "/content/mysite/de"]
}
```

### Cron expression reference

| Expression          | Meaning                                      |
| ------------------- | -------------------------------------------- |
| `0 0 * * * ?`       | Every hour at:00                             |
| `0 0/15 * * * ?`    | Every 15 minutes                             |
| `0 0 2 * * ?`       | Daily at 2:00 AM                             |
| `0 0 2 ? * MON-FRI` | Weekdays at 2:00 AM                          |
| `0 0 0 1 * ?`       | First day of each month at midnight          |
| `0 0/5 8-17 * * ?`  | Every 5 minutes during business hours (8-17) |

### Pattern 2: Sling Scheduler API (programmatic)

For dynamic scheduling (e.g., scheduling a task from a servlet or workflow):

```java
import org.apache.sling.commons.scheduler.Scheduler;
import org.apache.sling.commons.scheduler.ScheduleOptions;

@Reference
private Scheduler scheduler;

public void scheduleOneTimeTask(String taskId, long delayMs) {
    ScheduleOptions options = scheduler.AT(
        new Date(System.currentTimeMillis() + delayMs)
    );
    options.name(taskId);
    options.canRunConcurrently(false);

    scheduler.schedule(this::executeTask, options);
}

public void schedulePeriodic(String taskId, int intervalSeconds) {
    ScheduleOptions options = scheduler.EXPR("0 0/" + (intervalSeconds / 60) + " * * * ?");
    options.name(taskId);
    options.canRunConcurrently(false);

    scheduler.schedule(this::executeTask, options);
}

public void cancelSchedule(String taskId) {
    scheduler.unschedule(taskId);
}

private void executeTask() {
    // Task logic here
}
```

***

## Practical Examples

### Integration sync scheduler

Sync product data from a PIM every 30 minutes:

```java
@Component(service = Runnable.class, immediate = true)
@Designate(ocd = PimSyncScheduler.Config.class)
public class PimSyncScheduler implements Runnable {

    @ObjectClassDefinition(name = "PIM Sync Scheduler")
    @interface Config {
        @AttributeDefinition(name = "Cron expression")
        String scheduler_expression() default "0 0/30 * * * ?";

        @AttributeDefinition(name = "Concurrent")
        boolean scheduler_concurrent() default false;

        @AttributeDefinition(name = "PIM API endpoint")
        String pimEndpoint() default "";
    }

    @Reference
    private JobManager jobManager;

    private Config config;

    @Activate
    protected void activate(Config config) {
        this.config = config;
    }

    @Override
    public void run() {
        // The scheduler creates a Sling Job for the actual work
        // This ensures guaranteed execution with retries
        Map<String, Object> props = new HashMap<>();
        props.put("pimEndpoint", config.pimEndpoint());
        props.put("triggeredAt", System.currentTimeMillis());

        jobManager.addJob("com/myproject/jobs/pim-sync", props);
    }
}
```

> **Pattern:** The scheduler creates the job; the `JobConsumer` does the work. This separates scheduling concerns from execution and ensures retries on failure.

### Content cleanup job

Remove temporary content older than 30 days:

```java
@Component(
    service = JobConsumer.class,
    property = {
        JobConsumer.PROPERTY_TOPICS + "=com/myproject/jobs/content-cleanup"
    }
)
public class ContentCleanupJobConsumer implements JobConsumer {

    @Reference
    private ResourceResolverFactory resolverFactory;

    @Override
    public JobResult process(Job job) {
        int maxAgeDays = job.getProperty("maxAgeDays", 30);

        Map<String, Object> authInfo = Map.of(
            ResourceResolverFactory.SUBSERVICE, "cleanup-service"
        );

        try (ResourceResolver resolver = resolverFactory.getServiceResourceResolver(authInfo)) {
            Calendar cutoff = Calendar.getInstance();
            cutoff.add(Calendar.DAY_OF_MONTH, -maxAgeDays);

            // Find and delete old temporary content
            String query = String.format(
                "SELECT * FROM [nt:unstructured] WHERE ISDESCENDANTNODE('/tmp/myproject') " +
                "AND [jcr:created] < CAST('%s' AS DATE)",
                cutoff.toInstant().toString()
            );

            Iterator<Resource> results = resolver.findResources(query, "JCR-SQL2");
            int deleted = 0;

            while (results.hasNext()) {
                Resource resource = results.next();
                resolver.delete(resource);
                deleted++;

                // Batch commit every 100 deletes
                if (deleted % 100 == 0) {
                    resolver.commit();
                }
            }

            resolver.commit();
            log.info("Cleanup complete: deleted {} resources", deleted);
            return JobResult.OK;

        } catch (Exception e) {
            log.error("Cleanup failed", e);
            return JobResult.FAILED;
        }
    }
}
```

***

## Monitoring Jobs

### Felix Web Console

Navigate to `/system/console/slingevent` to see:

- Active queues and their configuration
- Queued, active, and finished jobs
- Failed jobs and retry counts
- Job statistics

### Groovy Console

```groovy
import org.apache.sling.event.jobs.JobManager

def jobManager = getService(JobManager.class)

// List all active jobs
def jobs = jobManager.findJobs(JobManager.QueryType.ACTIVE, null, 0, null)
jobs.each { job ->
    println "Topic: ${job.topic}, ID: ${job.id}, Created: ${job.created.time}"
}

// Count queued jobs for a specific topic
def count = jobManager.findJobs(
    JobManager.QueryType.QUEUED,
    "com/myproject/jobs/asset-import",
    0, null
).size()
println "Queued import jobs: $count"
```

### JMX

Sling Jobs expose JMX MBeans under `org.apache.sling:type=queues`. Monitor:

- Queue depth
- Processing times
- Failure rates
- Average wait times

***

## Sling Jobs vs EventHandler vs Workflow

| Aspect                   | Sling Jobs                  | EventHandler          | Workflow                       |
| ------------------------ | --------------------------- | --------------------- | ------------------------------ |
| **Guaranteed execution** | Yes                         | No                    | Yes                            |
| **Retries**              | Automatic (configurable)    | None                  | Manual (retry step)            |
| **Persistence**          | JCR (`/var/eventing/jobs`)  | None                  | JCR (`/var/workflow`)          |
| **Visual modelling**     | No                          | No                    | Yes (workflow editor)          |
| **User tasks**           | No                          | No                    | Yes (inbox, participant steps) |
| **Cluster**              | Runs on one node            | Runs on all nodes     | Runs on one node               |
| **Best for**             | Backend tasks, integrations | Lightweight reactions | Multi-step business processes  |

***

## Cluster Behaviour

In a clustered AEM setup (multiple author instances), Sling Jobs ensure each job runs on **exactly one node**:

- The Job Queue Manager distributes jobs across the cluster
- By default, any node can pick up any job
- Set `queue.preferRunOnCreationInstance = true` to prefer the originating node
- If a node goes down, its queued jobs are redistributed to surviving nodes

***

## AEMaaCS Considerations

- Sling Jobs work on AEMaaCS, but the **underlying storage differs** from on-prem
- Jobs are still persisted and survive restarts
- The Felix console (`/system/console/slingevent`) is available on author via Developer Console
- **Scheduler frequency limits**: avoid scheduling tasks more frequently than every 5 minutes on AEMaaCS; the infrastructure may throttle or de-duplicate
- For heavy background work, consider **Adobe I/O App Builder** actions triggered by I/O Events instead of long-running Sling Jobs

***

## Best Practices

### Separate scheduling from execution

The scheduler creates the job; the `JobConsumer` does the work. This gives you retries, monitoring, and cluster-awareness for free.

### Use meaningful job topics

Follow reverse-domain notation: `com/myproject/jobs/asset-import`. This makes filtering and monitoring easy.

### Keep jobs short

Long-running jobs (> 5 minutes) tie up queue threads. For bulk operations, break the work into smaller jobs or use `JobExecutor` with progress tracking.

### Always handle all three outcomes

Your `JobConsumer` should return `OK`, `FAILED`, or `CANCEL` based on the error type. Never swallow exceptions silently.

### Don't store large data in job properties

Store a path or ID, not the data itself. Job properties are persisted in the JCR.

### Use service users for jobs

Jobs run without a user session. Use `ResourceResolverFactory.getServiceResourceResolver()` with a dedicated service user.

***

## Common Pitfalls

| Pitfall                             | Solution                                                                                                  |
| ----------------------------------- | --------------------------------------------------------------------------------------------------------- |
| Jobs never execute                  | Check that a `JobConsumer` is registered for the topic; verify in Felix console                           |
| Jobs execute on all cluster nodes   | This is EventHandler behaviour, not Sling Jobs; make sure you're using `JobManager.addJob()`              |
| Queue is stuck / paused             | An ORDERED queue pauses when a job fails; check for failed jobs and fix or cancel them                    |
| Scheduler runs but nothing happens  | Verify the cron expression; check that `scheduler.concurrent` is set correctly                            |
| Scheduler runs too often on AEMaaCS | AEMaaCS may run multiple instances; use `scheduler.concurrent = false` and Sling Jobs for the actual work |
| Memory issues with bulk jobs        | Process in batches; commit the ResourceResolver periodically; don't accumulate results in memory          |
| Job retries endlessly               | Return `JobResult.CANCEL` for permanent failures; configure `queue.retries`                               |
