The official Go client for the Convilyn API —
file conversion, agentic goal workflows, and the community workflow library,
behind a single ck_ API key.
It is the Go-idiomatic sibling of the Python (convilyn) and TypeScript
(@convilyn/sdk) consumer SDKs: the same data-plane behaviour, rendered the
Go way — context.Context on every network call, (_, error) returns, typed
structs, io.Reader streaming, *http.Client injection, and
errors.Is / errors.As-friendly errors.
import convilyn "github.com/CoreNovus/convilyn-consumer-go"go get github.com/CoreNovus/convilyn-consumer-goRequires Go 1.22+. No third-party dependencies (standard library only).
package main
import (
"context"
"log"
"os"
"time"
convilyn "github.com/CoreNovus/convilyn-consumer-go"
)
func main() {
client, err := convilyn.NewClient(
convilyn.WithAPIKey(os.Getenv("CONVILYN_API_KEY")),
convilyn.WithTimeout(30 * time.Second),
convilyn.WithMaxRetries(2),
)
if err != nil {
log.Fatal(err)
}
defer client.Close()
ctx := context.Background()
file, err := client.Files.Upload(ctx, convilyn.UploadFileRequest{Path: "report.docx"})
if err != nil {
log.Fatal(err)
}
job, err := client.Convert.CreateAndWait(ctx, convilyn.ConvertCreateRequest{
File: file,
TargetFormat: "pdf",
})
if err != nil {
log.Fatal(err)
}
if _, err := client.Convert.DownloadTo(ctx, job, "report.pdf"); err != nil {
log.Fatal(err)
}
}Pass a consumer key with WithAPIKey, or set CONVILYN_API_KEY and omit it.
Keys start with ck_ and are sent as Authorization: Bearer ck_.... The key is
masked in every fmt verb and never appears in logs or error messages:
client, _ := convilyn.NewClient(convilyn.WithAPIKey("ck_live_abcdef...90"))
fmt.Printf("%+v\n", client) // convilyn.Client{baseURL:"...", apiKey:ck_l…90}Files.Upload runs the presign → S3 PUT → confirm flow and streams the body, so
large files are never buffered into memory. Upload from a path or any io.Reader:
// From a path (size + content-type inferred):
file, err := client.Files.Upload(ctx, convilyn.UploadFileRequest{Path: "data.csv"})
// From a stream:
f, _ := os.Open("big.mp4")
defer f.Close()
file, err = client.Files.Upload(ctx, convilyn.UploadFileRequest{
Reader: f,
Filename: "big.mp4",
Size: fileSize, // recommended for streams so S3 gets a Content-Length
})Goals.Run starts a job and polls until it finishes or pauses for human input:
job, err := client.Goals.Run(ctx, convilyn.GoalRunRequest{
WorkflowID: "wf_summarize",
Files: []string{file.ID},
})
if err != nil {
log.Fatal(err)
}
if job.NeedsInput() {
for _, slot := range job.PendingSlots {
fmt.Println("agent asks:", slot.Question)
}
}When a job pauses at slots_pending, answer the slots, confirm, and let it run —
or cancel / retry:
// Answer one slot (or FillSlots for several at once):
job, _ = client.Goals.FillSlot(ctx, job.JobSpecID, "sheet", "Q1")
// Confirm and wait for the run to finish:
job, _ = client.Goals.Confirm(ctx, job.JobSpecID)
job, _ = client.Goals.Wait(ctx, job.JobSpecID)
// Or cancel / retry:
job, _ = client.Goals.Cancel(ctx, job.JobSpecID)
job, _ = client.Goals.Retry(ctx, job.JobSpecID, convilyn.RetryOptions{RerunMode: convilyn.RerunFresh})Pass ExpectedVersion (from job.ItemVersion) to FillSlots/Confirm for
optimistic locking — a stale write returns a 409 *APIError.
Goals.Events opens a WebSocket and streams GoalEvents — tool calls, agent
steps, progress, and agent text — until a terminal event (completed / failed /
cancelled). It iterates with the same Next / Current / Err cursor shape as
the paginator:
stream, err := client.Goals.Events(ctx, job.JobSpecID)
if err != nil {
log.Fatal(err)
}
defer stream.Close()
for stream.Next() {
ev := stream.Current()
fmt.Println(ev.Type, ev.Seq)
}
if err := stream.Err(); err != nil {
log.Fatal(err) // a dropped connection surfaces as *WebSocketError
}Backend status: the WebSocket gateway does not yet accept
ck_keys for the event stream. Until that lands, prefer polling withGoals.Wait(which uses standard HTTP auth and works today);Eventsis wired and ready for when the gateway gainsck_support. There is no auto-reconnect — the backend does not replay missed events, so a silent reconnect would hide gaps.
Because Go has no standard-library WebSocket client, the SDK ships no default
transport: inject one implementing the WSTransport interface (Connect /
Send / Recv / Close) via WithWSTransport, and set the endpoint with
WithWSURL or the CONVILYN_WS_URL environment variable (a per-call
EventsOptions.WSURL overrides both). Without a transport, Events returns a
*WebSocketError.
page, err := client.Workflows.Search(ctx, convilyn.WorkflowSearchRequest{
Sort: "popular",
Limit: 50,
})
// Or iterate every page lazily:
it := client.Workflows.SearchAll(ctx, convilyn.WorkflowSearchRequest{Limit: 50})
for it.Next() {
fmt.Println(it.Current().WorkflowID)
}
if err := it.Err(); err != nil {
log.Fatal(err)
}Fork a curated/public source, edit it, publish it, or like one (some actions need
Pro tier — a 402 surfaces as *PlanRequiredError):
wf, _ := client.Workflows.Fork(ctx, convilyn.ForkRequest{SourceSpecID: "doc_analyzer"})
// Partial update (optimistic-locked on ItemVersion); only set fields are sent:
name := "My report pipeline"
wf, _ = client.Workflows.Patch(ctx, wf.WorkflowID, convilyn.PatchWorkflowRequest{
ItemVersion: wf.ItemVersion,
Name: &name,
Tags: []string{"reports"},
})
wf, _ = client.Workflows.Publish(ctx, wf.WorkflowID, wf.ItemVersion) // visibility → public
like, _ := client.Workflows.Like(ctx, "wf_community_123") // toggle; like.Liked is the new stateAll amounts are in credits (1 credit = $0.01):
est, _ := client.Account.GetQuota(ctx, convilyn.QuotaRequest{
Tools: []string{"pdf-mcp:extract_text"},
})
fmt.Printf("~%.2f credits (state=%s)\n", est.EstimatedCredits, est.QuotaCheck.State)
plan, _ := client.Account.GetPlan(ctx)
usage, _ := client.Account.UsageHistory(ctx)Every error is inspectable with errors.Is (category sentinels) and errors.As
(typed details):
_, err := client.Goals.Run(ctx, req)
var apiErr *convilyn.APIError
var quota *convilyn.QuotaExceededError
switch {
case errors.Is(err, convilyn.ErrRateLimited):
// back off and retry later
case errors.As(err, "a):
fmt.Printf("need %.2f credits, have %.2f — top up at %s\n",
quota.CostCredits, quota.BalanceCredits, quota.TopUpURL)
case errors.As(err, &apiErr):
fmt.Printf("status=%d code=%s request_id=%s\n",
apiErr.StatusCode, apiErr.Code, apiErr.RequestID)
}Sentinels: ErrAuthentication, ErrPermissionDenied, ErrRateLimited,
ErrTimeout, ErrQuotaExceeded, ErrPlanRequired. Typed errors: APIError,
RateLimitError, QuotaExceededError, PlanRequiredError, AuthError,
S3UploadError, RetryExhaustedError, JobFailedError, JobTimeoutError,
GoalJobFailedError, GoalJobTimeoutError.
- Timeout —
WithTimeout(d)sets a per-request timeout (default 30s); cancelling thectxaborts in flight. - Retries — transient failures (408/429/5xx, connection resets) are retried
with exponential backoff + jitter, honoring
Retry-After. Tune withWithMaxRetries(n)or supply a customRetryPolicy(e.g.NoRetry{}). - Idempotency — mutating requests carry an auto-generated
Idempotency-Key, stable across retries, so a retried create/run never double-charges. Disable withWithDisableIdempotency(true).
client, _ := convilyn.NewClient(
convilyn.WithAPIKey(key),
convilyn.WithHTTPClient(myHTTPClient), // proxy, tracing, httptest, ...
convilyn.WithBaseURL("http://localhost:8080"),
)This is the consumer SDK. Author/console concerns (tool servers, workflow-spec
compilation, MCP, inbound HMAC, cvl_ deploy tokens) live in the separate author
SDK, github.com/CoreNovus/convilyn-author-go.
The full consumer surface is implemented: Goals HITL actions (FillSlot /
FillSlots / Confirm / Cancel / Retry), the workflow mutations (Fork /
Publish / Patch / Like), and the Goals.Events WebSocket stream. Events
is wired and ready but gated on a backend dependency — the WS gateway does not
yet accept ck_ keys, and the SDK ships no default transport (inject a
WSTransport); prefer Goals.Wait polling until that lands. See the live
event-stream section above.
make test # go test ./... -cover
make race # go test -race ./...
make vet # go vet ./...
make fmtcheck # gofmt -l (must be empty)
make build # go build ./...