Skip to content
NorthstarPlatformPricingLogin

Best Practices

Patterns for reliable, efficient, and cost-effective Lightcone usage.

These patterns will help you build reliable workflows that handle real-world conditions. They’re distilled from common issues and proven approaches.

Always use with blocks for computers. They guarantee cleanup even if your code raises an exception:

# Good — computer always terminates
with client.computer.create(kind="desktop") as computer:
computer.click(100, 200)
# computer terminates when block exits, even on error
# Avoid — computer leaks if an error occurs before delete()
computer = client.computers.create(kind="desktop")
client.computers.click(computer.id, x=100, y=200)
client.computers.delete(computer.id) # never reached if click() throws
// Always use try/finally for cleanup
const computer = await client.computers.create({ kind: "desktop" });
const id = computer.id!;
try {
await client.computers.click(id, { x: 100, y: 200 });
} finally {
await client.computers.delete(id);
}

Choose timeouts based on your task, not defaults:

# Quick task — short timeout to avoid paying for idle computers
with client.computer.create(
kind="desktop",
timeout_seconds=120,
inactivity_timeout_seconds=30,
) as computer:
pass # ... do the task
# Long-running work — generous timeout with keepalive disabled
with client.computer.create(
kind="desktop",
timeout_seconds=3600,
auto_kill=False,
) as computer:
pass # ... do long-running work
// Quick task
const computer = await client.computers.create({
kind: "desktop",
timeout_seconds: 120,
inactivity_timeout_seconds: 30,
});
// Long-running work
const computer = await client.computers.create({
kind: "desktop",
timeout_seconds: 3600,
auto_kill: false,
});

Don’t spin up a new computer for every run when you can reuse one. Persistent computers save their full state — installed software, logged-in services, files:

# First run: create and save
with client.computer.create(kind="desktop", persistent=True) as computer:
client.computers.exec.sync(computer.id, command="apt-get install -y libreoffice")
# ... set up software, log in to services ...
computer_id = computer.id
# Save computer_id to a file or database
# Subsequent runs: restore
with client.computer.create(
kind="desktop",
environment_id=computer_id,
) as computer:
# Everything is still installed and configured
pass
// First run
const computer = await client.computers.create({
kind: "desktop",
persistent: true,
});
// ... set up, then save computer.id somewhere ...
await client.computers.delete(computer.id!);
// Subsequent runs
const restored = await client.computers.create({
kind: "desktop",
environment_id: savedComputerId,
});

Desktop layouts shift between loads and after actions. Never hardcode coordinates — always take a fresh screenshot to determine the current positions:

client.computers.exec.sync(computer.id, command="firefox https://example.com &")
computer.wait(3)
# Screenshot to see the current layout
result = computer.screenshot()
url = computer.get_screenshot_url(result)
print(f"Check layout: {url}")
# Use coordinates from the screenshot, not from a previous run
computer.click(500, 300)
await client.computers.exec.sync(id, { command: "firefox https://example.com &" });
await new Promise((r) => setTimeout(r, 3000));
const result = await client.computers.screenshot(id);
console.log("Check layout:", result.result?.screenshot_url);
await client.computers.click(id, { x: 500, y: 300 });

Prefer AI or selectors over coordinates when possible

Section titled “Prefer AI or selectors over coordinates when possible”

For more reliable element targeting, use the Responses API to let Northstar find click targets automatically from screenshots, or use the Playwright integration with CSS selectors for browser-mode computers.

After a critical action (clicking a button, submitting a form), take another screenshot to confirm it worked:

computer.click(400, 300) # Click "Submit"
computer.wait(2)
result = computer.screenshot()
url = computer.get_screenshot_url(result)
# Verify: did a confirmation dialog appear?

Applications need time to start. Always add a wait after launching software:

client.computers.exec.sync(computer.id, command="firefox https://example.com &")
computer.wait(3) # Wait for the app to launch and render
# For heavier applications, wait longer
client.computers.exec.sync(computer.id, command="libreoffice --calc &")
computer.wait(5)
await client.computers.exec.sync(id, { command: "firefox https://example.com &" });
await new Promise((r) => setTimeout(r, 3000));
// For heavier applications
await client.computers.exec.sync(id, { command: "libreoffice --calc &" });
await new Promise((r) => setTimeout(r, 5000));

Desktop environments need time to process each action. Add short waits between interactions that modify the screen:

computer.click(400, 300) # Click a dropdown
computer.wait(1) # Wait for dropdown to open
computer.click(400, 380) # Click an option
computer.wait(1) # Wait for selection to apply

When you have multiple actions that don’t need screenshots between them, use batch to execute them in a single request:

client.computers.batch(computer.id, actions=[
{"type": "click", "x": 400, "y": 300},
{"type": "type", "text": "search query"},
{"type": "hotkey", "keys": ["Enter"]},
])
await client.computers.batch(id, {
actions: [
{ type: "click", x: 400, y: 300 },
{ type: "type", text: "search query" },
{ type: "hotkey", keys: ["Enter"] },
],
});

Computers can time out, get terminated, or encounter network issues. Wrap your work in error handling:

import tzafon
from tzafon import Lightcone
client = Lightcone()
try:
with client.computer.create(kind="desktop") as computer:
computer.click(100, 200)
result = computer.screenshot()
except tzafon.APITimeoutError:
print("Request timed out — try increasing the SDK timeout")
except tzafon.APIStatusError as e:
if e.status_code == 404:
print("Computer not found — it may have timed out")
elif e.status_code == 429:
print("Rate limited — reduce concurrency or add delays")
else:
raise
import Lightcone from "@tzafon/lightcone";
try {
const computer = await client.computers.create({ kind: "desktop" });
// ...
} catch (e) {
if (e instanceof Lightcone.APIConnectionTimeoutError) {
console.error("Request timed out");
} else if (e instanceof Lightcone.APIError) {
console.error(`API error ${e.status}: ${e.message}`);
} else {
throw e;
}
}

The SDKs already retry on transient errors (429, 5xx) with exponential backoff — default 2 retries. Adding your own retry loop on top can cause excessive requests. Instead, increase the SDK’s retry limit if needed:

client = Lightcone(max_retries=5)
const client = new Lightcone({ maxRetries: 5 });

Vague instructions lead to unpredictable results. Be explicit about what “done” looks like:

# Vague — Northstar may give up or take the wrong path
client.agent.tasks.start(
instruction="Find cheap flights",
kind="desktop",
)
# Specific — Northstar knows exactly what to do and when it's done
client.agent.tasks.start(
instruction=(
"Open Firefox and go to google.com/travel/flights. "
"Search for round-trip flights from SFO to JFK, "
"departing March 15 and returning March 22. "
"Sort by price (lowest first). "
"You're done when the results page shows sorted flights."
),
kind="desktop",
max_steps=30,
)

Always set max_steps to a reasonable limit for your work:

Task typeSuggested max_steps
Simple navigation and screenshot5–10
Form filling10–20
Multi-step workflow20–40
Complex research40–100

Streaming lets you see what Northstar is doing in real time and intervene if it goes off track:

for event in client.agent.tasks.start_stream(
instruction="...",
kind="desktop",
max_steps=20,
):
if hasattr(event, "screenshot_url"):
print(f"Step screenshot: {event.screenshot_url}")
elif hasattr(event, "status"):
print(f"Status: {event.status}")
const stream = await client.agent.tasks.startStream({
instruction: "...",
kind: "desktop",
max_steps: 20,
});
for await (const event of stream) {
console.log(event);
}

Use the advanced proxy for sensitive sites

Section titled “Use the advanced proxy for sensitive sites”

Sites with bot detection (e-commerce, social media, banking) need the advanced proxy:

with client.computer.create(
kind="browser",
use_advanced_proxy=True,
) as computer:
computer.navigate("https://protected-site.com")

Even with the proxy, unnaturally fast interactions can trigger bot detection. Add realistic pauses:

import random
computer.click(400, 300)
computer.wait(random.uniform(0.5, 1.5)) # Human-like pause
computer.type("search query")
computer.wait(random.uniform(0.3, 0.8))
computer.hotkey("enter")

Lightcone gives you the power to interact with any website. Use that power responsibly — check a site’s robots.txt and terms of service before automating.