laravel-tembo maintained by cabuz
laravel-tembo
A Laravel SDK for the Tembo API.
Tembo runs coding agents (Claude Code, Codex, Cursor, Amp, Opencode...) in the background on your repositories, and they usually finish with a pull request. This package lets your Laravel app start those agents and react when they finish:
use Cabuz\Tembo\Facades\Tembo;
$session = Tembo::sessions()->start('Fix the N+1 query on the orders page', [
'codeRepositoryIds' => [$repositoryId],
]);
Tembo::sessions()->watch($session['id'], ['ticket_id' => $ticket->id]);
// In a listener: the agent is done.
public function handle(SessionCompleted $event): void
{
Ticket::find($event->context['ticket_id'])
->update(['pull_request' => $event->pullRequestUrls()[0] ?? null]);
}
- The whole public API: sessions, messages, agents (runs, schedules, triggers), projects, repositories, pull requests, integrations, MCP connections, skills, organizations, users, billing and more. A test checks every SDK method against Tembo's OpenAPI schema.
- Laravel-native: config file, facade, Laravel's HTTP client, so
Http::fake()andTembo::fake()work in your tests. - Session events without webhooks: a queued job watches a session and
fires
SessionCompleted,SessionFailedorSessionCancelled. - Live output: stream what the agent is doing over Tembo's WebSocket API, with automatic reconnects.
- Safe retries: rate limits are retried for every request, but server errors and dropped connections only for reads, so a retry never starts the same agent twice.
- Typed exceptions for every error status, and cursor pagination that can stream every page lazily.
- Artisan commands to check your key and start an agent from the terminal.
This is a community package, not an official Tembo SDK. Tembo's own SDK is TypeScript only.
Requirements
- PHP 8.1+
- Laravel 10.10+, 11, 12 or 13
- A Tembo API key
Installation
composer require cabuz/laravel-tembo
Create an API key in the Tembo dashboard under Settings > API Keys and add
it to .env:
TEMBO_API_KEY=your-key
Check that it works:
php artisan tembo:check
To change the defaults, publish the config file:
php artisan vendor:publish --tag=tembo-config
Usage
Use the Tembo facade, or inject Cabuz\Tembo\Tembo. Every method returns the
decoded JSON as an array; list methods return a CursorPage.
Start a coding agent
$session = Tembo::sessions()->start('Add rate limiting to the public API', [
'codeRepositoryIds' => ['123e4567-e89b-12d3-a456-426614174000'],
'agent' => 'claudeCode:claude-fable-5-1', // optional, see Tembo::models()->list()
'baseBranch' => 'develop',
]);
$session['id']; // session UUID
$session['htmlUrl']; // link to the session in Tembo
start() creates the session and queues the task right away. Use
create() to pass the raw request body yourself.
Look up repository IDs by name:
$repository = Tembo::repositories()->findByName('acme/api');
Follow up, steer or stop
Tembo::sessions()->sendMessage($id, 'Also add a feature test');
Tembo::sessions()->sendMessage($id, 'Use the existing RateLimiter', ['deliveryMode' => 'steer']);
Tembo::sessions()->stop($id);
Tembo::sessions()->fork($id, ['prompt' => 'Try a different approach']);
Wait for the result
Tembo has no webhooks yet. There are two ways to know when an agent is done.
In a queue (recommended). watch() dispatches a job that polls the session
every 30 seconds and fires an event when it ends. No worker is blocked while
the agent runs.
Tembo::sessions()->watch($session['id'], context: ['ticket_id' => 42]);
| Event | When |
|---|---|
Cabuz\Tembo\Events\SessionCompleted |
The agent finished. |
Cabuz\Tembo\Events\SessionFailed |
The agent failed. |
Cabuz\Tembo\Events\SessionCancelled |
The session was stopped. |
Cabuz\Tembo\Events\SessionWatchTimedOut |
Still running after the watch timeout (1 hour by default). |
Every event has $sessionId, $session (the latest API data), $context
(what you passed to watch()), state() and pullRequestUrls(). All four
implement Cabuz\Tembo\Events\SessionFinished, so one listener on that
interface receives every outcome.
Synchronously. wait() polls in the current process and returns the final
session. Use it in commands and jobs, not in web requests.
$session = Tembo::sessions()->wait($id, timeout: 900, interval: 15);
\Cabuz\Tembo\Support\SessionState::of($session); // "completed", "failed" or "cancelled"
It throws SessionTimeoutException when the timeout passes.
Stream the live output
Follow what the agent is doing as it happens, over Tembo's WebSocket API. This needs a WebSocket client:
composer require phrity/websocket
foreach (Tembo::sessions()->events()->live()->subscribe($sessionId) as $frame) {
// "ready" once subscribed, "delta" for each output event,
// "idle" after 15 seconds without output.
if ($frame['type'] === 'delta') {
logger()->info($frame['data']['message']);
}
// Break whenever you're done; the connection is closed for you.
}
delta frames carry event, message (the agent provider's event payload as
a string) and, when known, provider, messageId and createdAt. idle
frames are specific to this package: PHP can't run a timer during a blocking
read, so the loop gets a turn to check other things, such as
Tembo::sessions()->state($id), while the agent is quiet.
Dropped connections are retried with a new ticket (up to 5 times, with
backoff); a rejected subscription throws LiveSubscriptionException. It's a
live stream, not a replay: use Tembo::sessions()->listEvents($id) for
anything missed during a reconnect. Options: maxRetries, initialDelay and
maxDelay (milliseconds) and timeout (seconds).
For several subscriptions on one connection, use the raw API:
Tembo::live()->authorize() returns a ticket, Tembo::live()->connect($ticket)
opens the connection, and send() / receive() exchange
the protocol's frames.
Automation agents
$agent = Tembo::agents()->create([
'name' => 'Dependency updater',
'instructions' => 'Update outdated Composer packages and open a PR.',
'repositoryIds' => [$repositoryId],
]);
Tembo::agents()->schedules()->create($agent['id'], ['cron' => '0 6 * * 1']);
// Run it now; the payload reaches the agent as custom input.
$run = Tembo::agents()->run($agent['id'], ['reason' => 'Security advisory CVE-2026-1234']);
Everything else
The structure and method names follow Tembo's official
TypeScript SDK.
api.md lists every method with the endpoint it calls. Each resource has list(), create(),
retrieve(), update() and delete() where the API supports them. Nested
resources are reached through an accessor and take the parent ID first:
Tembo::agents()->runs()->retrieve($agentId, $runId);
Tembo::sessions()->diffs()->list($sessionId);
Tembo::organizations()->secrets()->create($organizationId, ['key' => 'NPM_TOKEN', 'value' => '...']);
Tembo::projects()->environmentVariables()->create($projectId, ['APP_ENV' => 'testing']);
Tembo::users()->profilePicture()->update($userId, $request->file('avatar'));
| Accessor | Nested resources and extra methods |
|---|---|
sessions() |
sources(), diffs(), inputs(), queue(), commands(); fork(), stop(), listEvents() |
messages() |
Session messages |
agents() |
templates(), schedules(), triggers(), runs(), contentRevisions() |
projects() |
environmentVariables(), snapshots(), builds(), schedules(); duplicate(), updateDefaults() |
repositories() |
Repositories |
pullRequests() |
Pull requests opened by sessions |
artifacts() |
Session artifacts |
integrations() |
providers(), triggers(); authorize(), test(), sync(), syncAll(), retrieveSyncStatus(), retrieveRateLimit(), listSentryEnvironments(), listSlackChannels() |
mcpConnections() |
authorize(), test() |
skills() |
marketplace() |
models(), runtimes() |
Available agent models and runtimes |
organizations() |
members(), secrets(), settings(), subscriptions() |
users() |
settings(), connectedAccounts(), subscriptions(), profilePicture(); me() |
apiKeys() |
API keys |
billing() |
retrieve(), listUsage() |
insights() |
retrieve(), update(), listMembers(), listRepositories() |
pins() |
Pins |
For an endpoint Tembo adds before this package wraps it, use the raw methods:
Tembo::get('v1/some-new-endpoint', ['limit' => 10]);
Tembo::post('v1/some-new-endpoint', ['name' => 'x']);
Pagination
List methods return a CursorPage:
$page = Tembo::sessions()->list(['state' => 'active', 'limit' => 50]);
foreach ($page as $session) { /* items on this page */ }
$page->hasMore();
$next = $page->nextPage();
// Every item across all pages, fetched on demand:
Tembo::repositories()->list()->lazy()->each(fn ($repository) => ...);
List filters can be passed as PHP arrays and booleans; the package turns them
into the comma-separated values and true/false strings Tembo expects.
Multiple API keys
Use withApiKey() to act as another Tembo organization, for example one per
tenant. It returns a copy and leaves the default client untouched.
Tembo::withApiKey($tenant->tembo_key)->sessions()->start('...');
Watched sessions keep using that key. The key travels inside the queued job,
so the job payload is encrypted with your APP_KEY.
Per-client options
Like the official SDK's client and request options, these return a copy with one setting changed:
Tembo::withTimeout(120)->sessions()->list(); // seconds
Tembo::withRetries(0)->agents()->run($agentId); // no retries
Tembo::withHeaders(['X-Request-Id' => $requestId])->sessions()->retrieve($id);
Tembo::withQuery(['limit' => 100])->repositories()->list();
They can be chained, and withConfig() replaces any config value. The
Authorization header always comes from the API key. Default headers and query
parameters for every request go in the headers and query config keys.
Errors
Every error extends Cabuz\Tembo\Exceptions\TemboException:
| Exception | Status |
|---|---|
InvalidRequestException |
400, 413, 422 |
AuthenticationException |
401 |
PermissionDeniedException |
403 |
NotFoundException |
404 |
ConflictException |
409 |
RateLimitException |
429, after retries (retryAfter() gives the seconds) |
ServerException |
5xx |
ConnectionException |
Tembo could not be reached |
LiveSubscriptionException |
A live subscription was rejected (getCode() gives the status) |
MissingApiKeyException |
No API key configured |
MissingWebSocketClientException |
Live streaming without phrity/websocket installed |
try {
Tembo::sessions()->retrieve($id);
} catch (NotFoundException $e) {
$e->error(); // Tembo's error message
$e->status(); // 404
$e->response; // the Illuminate\Http\Client\Response
}
For a successful call, Tembo::lastResponse() returns the raw response of the
most recent request (status, headers such as rate limits, body), like the
official SDK's withResponse().
Retries
A request is retried up to 2 times (TEMBO_RETRIES), with a delay that
doubles after each attempt and follows Retry-After when Tembo sends it:
- 429 Too Many Requests: retried for every request, since Tembo didn't process it.
- 5xx and connection errors: retried for
GETrequests only. A failedPOSTmay already have started an agent, so it is never repeated.
Logging
Like the official SDK's TEMBO_LOG, set the lowest level to log:
TEMBO_LOG=debug
TEMBO_LOG_CHANNEL=stack # optional, defaults to your default channel
| Level | Logs |
|---|---|
debug |
Every request: method, path, query, status and duration |
info |
Retries |
warning |
Failed requests (4xx) |
error |
Server errors (5xx) and connection failures |
Each level includes the ones below it. Request bodies and the API key are never logged. Logging is off by default.
Artisan commands
# Check the API key
php artisan tembo:check
# Start an agent; --repo takes an ID or owner/name and can be repeated
php artisan tembo:run "Fix the flaky CheckoutTest" --repo=acme/shop --wait
# Or print the agent's output while it works (needs phrity/websocket)
php artisan tembo:run "Fix the flaky CheckoutTest" --repo=acme/shop --follow
tembo:run also accepts --agent, --project, --branch and --timeout.
Testing your app
Tembo::fake() stubs the API with paths relative to it. Requests that don't
match get an empty 200, so tests never reach Tembo. Assert on requests with
Laravel's Http assertions.
use Cabuz\Tembo\Facades\Tembo;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http;
Tembo::fake([
'v1/sessions' => ['id' => 'session-1', 'htmlUrl' => 'https://app.tembo.io/...'],
'v1/sessions/*' => ['id' => 'session-1', 'state' => ['current' => 'completed']],
]);
// ... exercise your code ...
Http::assertSent(fn (Request $request) => $request->url() === 'https://api.tembo.io/v1/sessions'
&& $request['description'] === 'Fix the login bug');
Patterns match the full URL, query string included, so use a trailing * for
requests with filters ('v1/repositories*'). Values can also be
Http::response(...) or Http::sequence() stubs. For the watcher, use
Queue::fake() and Event::fake() as usual.
Tembo::fakeLive() fakes the live stream: subscribe() yields ready, a
delta for each event you pass, then idle frames.
$live = Tembo::fakeLive([
['event' => 'output', 'message' => 'Running the tests'],
]);
// ... exercise your code ...
$live->assertSubscribed($sessionId);
A loop that never breaks fails with LiveSubscriptionException after 100 idle
frames instead of hanging the test suite. For full control (dropped
connections, errors), bind your own Cabuz\Tembo\Live\SocketFactory in the
container.
Configuration
| Key | .env |
Default |
|---|---|---|
api_key |
TEMBO_API_KEY |
none |
base_url |
TEMBO_BASE_URL |
https://api.tembo.io |
timeout |
TEMBO_TIMEOUT |
30 seconds |
retries |
TEMBO_RETRIES |
2 |
retry_delay |
TEMBO_RETRY_DELAY |
500 ms |
watch.interval |
TEMBO_WATCH_INTERVAL |
30 seconds |
watch.timeout |
TEMBO_WATCH_TIMEOUT |
3600 seconds |
watch.connection |
TEMBO_QUEUE_CONNECTION |
default connection |
watch.queue |
TEMBO_QUEUE |
default queue |
log |
TEMBO_LOG |
none (off) |
log_channel |
TEMBO_LOG_CHANNEL |
default channel |
headers |
none | [] |
query |
none | [] |
AI assistants
The package ships a Laravel Boost
guideline and skill, like the SKILL.md of the official SDK. Run
php artisan boost:install (or boost:update) and select
cabuz/laravel-tembo, and your AI assistant learns how to call Tembo with this
package instead of guessing.
Changelog
See CHANGELOG.md.
Contributing
See CONTRIBUTING.md. When Tembo ships new endpoints,
bin/update-endpoints refreshes the schema snapshot and the test suite lists
what's missing.
Security
Please report security issues privately, as described in SECURITY.md.
License
The MIT License (MIT). See LICENSE.