Join the Crew

Build a Trade Career You Are Actually Proud Of

We are building a team where people like coming to work, earn properly, and take pride in what they leave behind. If you would rather do the job right than do it fast, you will fit in here.

north_east
north_east
Billy's Vision

A Trade Business Built for the People Doing the Work

Billy started OctoPlus in 2005 after walking away from a job he could not stand behind. The point was never to build the biggest plumbing company in Brisbane. It was to build one where the work is honest, the standard is high, and nobody dreads Monday.

Call our experts now
Openings

Current roles

We are hiring people who take pride in their work and want to build something real. If you do not see your role listed, send your resume through anyway. We keep good people on file.

Trade Assistant

No ticket needed. Load out, dig, and learn the trade on site with a crew that actually explains what they're doing and why.

Entry Level
Full-time
Morningside
Apply Now

Hot Water Installer

Changeovers, heat pump installs and rebate paperwork done properly. Mostly booked work, minimal after-hours, van and fuel card included.

Hot Water
Full-time
Southside
Apply Now

Drainage & CCTV Technician

Camera work, jetting and pipe relining across residential and light commercial. Experience on a jet rod trailer preferred, training provided if not.

Drainage
Full-time
Brisbane
Apply Now

Service Coordinator

The first voice customers hear. Booking jobs, holding the two-hour windows, and keeping the crew's day running from our Morningside office.

Office
Part-time
Morningside
Apply Now

Plumbing Apprentice

Second or third year, on real jobs alongside licensed tradies from day one. Course fees and tickets covered.

Plumbing
Apprenticeship
Brisbane
Apply Now

Licensed Plumber / Gasfitter

Residential plumbing and gas across Brisbane's inner east. Day work with an on-call rotation, a properly stocked van, and no pressure to upsell.

Gasfitting
Full-time
Morningside
Apply Now

Think You Would Be a Good Fit?

Send your resume through and tell us about the work you are proud of. If there is a fit, we will call you for a chat about where you want to take your trade. If the timing is not right, we will keep your details on file.

north_east
/* ========================================= Lumos GSAP Starter Interactions Version: v1.1 ========================================= */ document.addEventListener("DOMContentLoaded", () => { /* ========================================= Validación inicial de GSAP ========================================= */ if (typeof gsap === "undefined") return; gsap.registerPlugin(ScrollTrigger); const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; if (prefersReducedMotion) return; /* ========================================= Animaciones de counters, cards e íconos ========================================= */ function initCounters() { /* Elementos */ const cards = document.querySelectorAll(".counter_card"); const counters = document.querySelectorAll(".counter_digit"); const icons = document.querySelectorAll(".counter_icon"); if (!cards.length && !counters.length && !icons.length) return; /* ----------------------------------------- Animación de entrada de las cards ----------------------------------------- */ if (cards.length) { gsap.fromTo(cards, { y: 32, opacity: 0, scale: 0.96, filter: "blur(8px)" }, { y: 0, opacity: 1, scale: 1, filter: "blur(0px)", duration: 0.75, ease: "power3.out", stagger: 0.12, scrollTrigger: { trigger: cards[0], start: "top 85%", once: true } } ); } /* ----------------------------------------- Animación numérica de los counters ----------------------------------------- */ counters.forEach((counter) => { const originalValue = counter.textContent.trim(); // Normaliza el número para aceptar separadores de miles const numericValue = originalValue.replace(/,/g, ""); const endValue = parseFloat(numericValue); if (isNaN(endValue)) return; const before = counter.dataset.before || ""; const after = counter.dataset.after || ""; const duration = parseFloat(counter.dataset.duration) || 2.2; const delay = parseFloat(counter.dataset.delay) || 0.2; // Detecta los decimales desde el valor numérico real. // Opcionalmente se pueden definir manualmente con data-decimals. const decimals = counter.dataset.decimals !== undefined ? Math.max(0, parseInt(counter.dataset.decimals, 10) || 0) : (String(endValue).split(".")[1] || "").length; // Mantiene separadores de miles y decimales const formatter = new Intl.NumberFormat("en-US", { minimumFractionDigits: decimals, maximumFractionDigits: decimals }); let obj = { value: 0 }; gsap.to(obj, { value: endValue, duration, delay, ease: "power4.out", scrollTrigger: { trigger: counter, start: "top 85%", once: true }, // Actualiza el texto mientras avanza la animación onUpdate: () => { counter.textContent = before + formatter.format(obj.value) + after; }, // Ajusta el valor final y agrega un pequeño rebote onComplete: () => { counter.textContent = before + formatter.format(endValue) + after; gsap.fromTo(counter, { scale: 1 }, { scale: 1.08, duration: 0.18, yoyo: true, repeat: 1, ease: "power2.out" } ); } }); }); /* ----------------------------------------- Animación de íconos ----------------------------------------- */ icons.forEach((icon, index) => { const iconInner = icon.querySelector(".material_icon") || icon; const tl = gsap.timeline({ scrollTrigger: { trigger: icon, start: "top 85%", once: true }, delay: 0.35 + index * 0.12 }); tl.fromTo(iconInner, { scale: 0, rotate: -20, opacity: 0, filter: "blur(8px)" }, { scale: 1.18, rotate: 0, opacity: 1, filter: "blur(0px)", duration: 0.55, ease: "back.out(2.4)" } ) .to(iconInner, { scale: 1, duration: 0.25, ease: "power2.out" }) .to(iconInner, { y: -4, duration: 1.8, repeat: -1, yoyo: true, ease: "sine.inOut" }); }); } /* ========================================= Inicialización general ========================================= */ initCounters(); initAutoTabs(); initHeadingReveal(); });