Quickstart
Your first cited interpretation.
From zero to a cited answer in minutes.
1. Mint an API key
In the PLCs.ai app, open Settings → API Keys, click Create key, and give it the ai_explain (Interpret & explain) scope. Copy the secret — it is shown exactly once.
export PLCS_API_KEY=plck_live_xxxxxxxxxxxxxxxxxxxxxxxx
export PLCS_PROJECT_ID=prj_your_project_id2. Install an SDK (optional)
pip install plcsaidotnet add package PlcsAi3. Get a cited answer
Using an example question — "What conditions must be true for the main conveyor to start?"
curl -s -X POST "https://app.plcs.ai/api/v1/projects/$PLCS_PROJECT_ID/interpret" \
-H "Authorization: Bearer $PLCS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"prompt": "What conditions must be true for the main conveyor to start?",
"mode": "sync"
}'from plcsai import Client
client = Client(api_key="plck_live_…")
result = client.interpret(
project_id="prj_…",
prompt="What conditions must be true for the main conveyor to start?",
)
if result.status == "answer":
print(result.answer)
for c in result.citations:
print(" -", c.location_kind, c.path)
elif result.status == "needs_input":
# The assistant stopped to ask. Relay the questions; nothing is guessed.
for q in result.questions:
print(q.question, q.options)
print(result.request_id)using PlcsAi;
var client = new PlcsClient("plck_live_…");
var result = await client.InterpretAsync(
projectId: "prj_…",
prompt: "What conditions must be true for the main conveyor to start?");
if (result is AnswerResponse answer)
{
Console.WriteLine(answer.Answer);
foreach (var c in answer.Citations)
Console.WriteLine($" - {c.LocationKind} {c.Path}");
}
else if (result is NeedsInputResponse ask)
{
// The assistant stopped to ask. Relay ask.Questions; nothing is guessed.
}
Console.WriteLine(result.RequestId);4. The response
Branch on status. A successful call is one of three shapes, and answer is only one of them:
{
"status": "answer",
"conversation_id": "v1-interpret-9f2a…",
"answer": "The main conveyor starts when the Start pushbutton is pressed, no E-Stop is active, and the upstream Ready interlock is set …",
"citations": [
{ "location_kind": "rung", "path": "ConveyorControl", "rung": 12 },
{ "location_kind": "tag", "path": "Conveyor_Start_PB" }
],
"usage": { "input_tokens": 1840, "output_tokens": 412 }
}5. The assistant may ask first
The assistant is interactive, so an ambiguous question comes back as needs_input rather than a guess:
{
"status": "needs_input",
"conversation_id": "v1-interpret-9f2a…",
"questions": [
{
"id": "which_line",
"question": "Which conveyor did you mean?",
"options": ["Line 1 infeed", "Line 2 discharge"],
"recommended_index": 1,
"why_it_matters": "The two are interlocked differently."
}
],
"usage": { "input_tokens": 620, "output_tokens": 84 }
}recommended_index is a hint, not a decision — nothing is applied on your behalf. A headless caller may read it and choose for itself, but the choice is yours.
6. Answer, and continue the turn
Collect a response, then call POST /projects/{id}/interpret again with the same conversation_id and an answers array. Send prompt only on the first call — the continuation carries answers instead:
{
"conversation_id": "v1-interpret-9f2a…",
"answers": [
{ "id": "which_line", "selected_index": 1 },
{ "id": "depth", "free_text": "just the rung that gates the enable" }
]
}Set exactly one of selected_index (zero-based into that question’s options) or free_text (when none of the options fit). If both are present the selection wins. An entry that answers nothing is dropped and the assistant asks again rather than guessing — so a partial batch is safe, not silently mis-answered.
from plcsai import Answer
if result.status == "needs_input":
answered = client.interpret(
project_id="prj_…",
conversation_id=result.conversation_id,
answers=[Answer("which_line", 1)], # free text: Answer("depth", free_text="…")
)
if answered.status == "answer":
print(answered.answer)if (result is NeedsInputResponse ask)
{
var answered = await client.InterpretAsync(
projectId: "prj_…",
conversationId: ask.ConversationId,
answers: new[] { new Answer("which_line", 1) }); // or new Answer("depth", "free text")
if (answered is AnswerResponse done) Console.WriteLine(done.Answer);
}The third shape, unresolved, means the request could not be served as asked — most often it asked to change the project, and names generate instead. Generate relays questions the same way, with the same answers payload.
Want tokens as they generate? Set "mode": "stream" and read the SSE events — see Streaming.